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    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
787    "pg_description",
788    "pg_enum",
789    "pg_extension",
790    // v7.39 (round 541) — pg_dump reads it for every relation of kind
791    // 'f'. SPG has no foreign tables, so it is empty, which is also
792    // what PG reports on a database that has none.
793    "pg_foreign_table",
794    // v7.39 (round 541) — the empty-by-truth family; see
795    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
796    "pg_event_trigger",
797    "pg_file_settings",
798    "pg_foreign_data_wrapper",
799    "pg_foreign_server",
800    "pg_hba_file_rules",
801    "pg_ident_file_mappings",
802    "pg_init_privs",
803    "pg_parameter_acl",
804    "pg_prepared_xacts",
805    "pg_publication_namespace",
806    "pg_publication_rel",
807    "pg_publication_tables",
808    "pg_replication_origin",
809    "pg_replication_origin_status",
810    "pg_seclabel",
811    "pg_seclabels",
812    "pg_shdepend",
813    "pg_shdescription",
814    "pg_shmem_allocations",
815    "pg_shmem_allocations_numa",
816    "pg_shseclabel",
817    "pg_statistic_ext_data",
818    "pg_stats_ext",
819    "pg_stats_ext_exprs",
820    "pg_subscription_rel",
821    "pg_transform",
822    "pg_user_mapping",
823    "pg_user_mappings",
824    "pg_index",
825    "pg_indexes",
826    "pg_inherits",
827    // v7.39 (round 650) — the text-search catalogs SPG can fill
828    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
829    // token types to dictionaries and SPG has no token-type model,
830    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
831    "pg_ts_config",
832    "pg_ts_config_map",
833    "pg_ts_dict",
834    "pg_ts_parser",
835    "pg_ts_template",
836    "pg_matviews",
837    "pg_namespace",
838    // v7.39 (round 621)
839    "pg_operator",
840    "pg_policies",
841    "pg_policy",
842    "pg_proc",
843    "pg_publication",
844    "pg_replication_slots",
845    "pg_roles",
846    // v7.39 (round 143) — the rewrite-rule listing view.
847    // v7.39 (round 312) — and the rule catalogue itself, which
848    // `pg_get_ruledef(oid)` resolves against.
849    "pg_rewrite",
850    "pg_rules",
851    "pg_sequence",
852    "pg_settings",
853    "pg_stat_archiver",
854    "pg_stat_bgwriter",
855    "pg_stat_checkpointer",
856    "pg_stat_database",
857    "pg_stat_io",
858    "pg_stat_progress_analyze",
859    "pg_auth_members",
860    "pg_stat_progress_create_index",
861    "pg_stat_progress_vacuum",
862    "pg_stat_replication",
863    "pg_stat_slru",
864    "pg_stat_subscription_stats",
865    "pg_stat_user_functions",
866    "pg_stat_user_indexes",
867    "pg_stat_user_tables",
868    "pg_stat_wal",
869    "pg_prepared_statements",
870    "pg_largeobject",
871    "pg_largeobject_metadata",
872    "pg_statistic",
873    "pg_statistic_ext",
874    "pg_subscription",
875    "pg_tables",
876    "pg_tablespace",
877    // v7.39 (round 502) — the timezone catalogues. SPG resolved
878    // named zones correctly but could not list them, so a client
879    // populating a timezone picker got "relation does not exist".
880    "pg_timezone_abbrevs",
881    "pg_timezone_names",
882    "pg_trigger",
883    "pg_type",
884    "pg_user",
885    "pg_views",
886];
887
888const MAX_NEST_DEPTH: usize = 64;
889
890/// Stack accounting for the nesting budget, test-only.
891///
892/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
893/// that MOVES: a compiler upgrade grew the parser's debug frames and
894/// silently ate the margin until `nesting_budget_errors_cleanly` went
895/// from erroring cleanly to aborting on a stack overflow. A count
896/// cannot notice that on its own, so the budget is measured here and
897/// held to a ceiling.
898///
899/// The reading has to come from a helper whose OWN frame is the same at
900/// every call: debug slot placement does not follow source order, so a
901/// local's address inside the function under test is not that
902/// function's frame boundary. Two earlier probes were wrong that way —
903/// one read `&self.nest_depth`, which is the `Parser`'s address and
904/// never moves at all.
905#[cfg(test)]
906mod frame_meter {
907    extern crate std;
908    use std::cell::Cell;
909
910    // Per-THREAD, not global. `cargo test` runs tests in parallel and
911    // plenty of them parse nested expressions, so shared statics get
912    // stack addresses from several threads at once and the subtraction
913    // below turns into noise — it read 229,772 bytes per level that way,
914    // while passing when the test was run on its own.
915    std::thread_local! {
916        static AT_LO: Cell<usize> = const { Cell::new(0) };
917        static AT_HI: Cell<usize> = const { Cell::new(0) };
918    }
919
920    pub(super) const SAMPLE_LO: usize = 4;
921    pub(super) const SAMPLE_HI: usize = 24;
922
923    #[inline(never)]
924    pub(super) fn record(depth: usize) {
925        let anchor = 0u8;
926        let at = core::ptr::from_ref(&anchor) as usize;
927        if depth == SAMPLE_LO {
928            AT_LO.with(|c| c.set(at));
929        } else if depth == SAMPLE_HI {
930            AT_HI.with(|c| c.set(at));
931        }
932    }
933
934    /// Bytes of stack one nesting level costs, averaged over the span.
935    pub(super) fn bytes_per_level() -> usize {
936        let lo = AT_LO.with(Cell::get);
937        let hi = AT_HI.with(Cell::get);
938        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
939        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
940        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
941    }
942
943    pub(super) fn reset() {
944        AT_LO.with(|c| c.set(0));
945        AT_HI.with(|c| c.set(0));
946    }
947}
948
949/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
950/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
951#[inline(never)]
952fn build_center_call(e: Expr) -> Expr {
953    Expr::FunctionCall {
954        name: alloc::string::String::from("center"),
955        args: alloc::vec![e],
956    }
957}
958
959/// Max consecutive binary operators at ONE precedence level
960/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
961/// parse time but evaluates and drops recursively — depth beyond
962/// this overflows 2 MiB worker stacks (debug eval frames run
963/// multiple KiB). `IN (…)` lists are flat and unaffected.
964const MAX_BINARY_CHAIN: usize = 256;
965
966/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
967/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
968/// it keeps its dedicated path (`parse_table_level_fk`).
969enum NamedTableConstraintKind {
970    Check,
971    Unique,
972    PrimaryKey,
973    Exclude,
974}
975
976impl Parser {
977    fn new(tokens: Vec<Token>) -> Self {
978        Self::new_with_dialect(tokens, false)
979    }
980
981    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
982        Self {
983            tokens,
984            mysql_dialect,
985            in_order_by_key: false,
986            order_key_collation: None,
987            pos: 0,
988            nest_depth: 0,
989            pending_sample_preds: Vec::new(),
990            suppress_in_tail: false,
991            last_consumed: 0,
992            src: None,
993        }
994    }
995
996    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
997    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
998        if self.mysql_dialect {
999            self.src = Some((input.to_string(), offsets.to_vec()));
1000        }
1001        self
1002    }
1003
1004    /// The source text spanning tokens `start ..= end`, trimmed.
1005    ///
1006    /// The offsets are token STARTS, so the span runs to the start of the
1007    /// token after `end` and gives back the whitespace between them —
1008    /// trimming is what makes `a + b FROM t` end at `b`.
1009    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1010        let (text, offsets) = self.src.as_ref()?;
1011        let from = *offsets.get(start)?;
1012        let to = *offsets.get(end + 1)?;
1013        text.get(from..to).map(str::trim_end)
1014    }
1015
1016    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1017    /// nesting depth, erroring out cleanly past the budget.
1018    fn enter_nested(&mut self) -> Result<(), ParseError> {
1019        self.nest_depth += 1;
1020        #[cfg(test)]
1021        frame_meter::record(self.nest_depth);
1022        if self.nest_depth > MAX_NEST_DEPTH {
1023            self.nest_depth -= 1;
1024            return Err(self.err(alloc::format!(
1025                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1026            )));
1027        }
1028        Ok(())
1029    }
1030
1031    fn peek(&self) -> &Token {
1032        // tokens always ends with Eof; pos is clamped in advance().
1033        &self.tokens[self.pos]
1034    }
1035
1036    fn advance(&mut self) -> Token {
1037        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1038        self.last_consumed = self.pos;
1039        if self.pos + 1 < self.tokens.len() {
1040            self.pos += 1;
1041        }
1042        t
1043    }
1044
1045    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1046    /// returned. It was computed as `pos - 1`, which is wrong at both
1047    /// ends: `advance()` parks on the final Eof rather than running off
1048    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1049    /// input`), and after backtracking `pos` is no longer one past the
1050    /// token that failed. Recorded by `advance()` itself instead.
1051    const fn consumed_pos(&self) -> usize {
1052        self.last_consumed
1053    }
1054
1055    fn err(&self, message: String) -> ParseError {
1056        ParseError {
1057            message,
1058            token_pos: self.pos,
1059        }
1060    }
1061
1062    fn expect_eof(&self) -> Result<(), ParseError> {
1063        if matches!(self.peek(), Token::Eof) {
1064            Ok(())
1065        } else {
1066            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1067        }
1068    }
1069
1070    /// v7.14.0 — swallow every token up to (but not including) the
1071    /// next semicolon / EOF. Used by the dump-noise dispatcher
1072    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1073    /// etc. without modeling each grammar.
1074    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1075    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1076    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1077    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1078    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1079    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1080        let start = self.pos;
1081        self.advance(); // COMMENT
1082        if !matches!(self.peek(), Token::On) {
1083            self.pos = start;
1084            self.consume_until_statement_boundary();
1085            return Ok(Statement::Empty);
1086        }
1087        self.advance(); // ON
1088        let kind = match self.peek() {
1089            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1090            Token::Table => "table".into(),
1091            _ => {
1092                self.consume_until_statement_boundary();
1093                return Ok(Statement::Empty);
1094            }
1095        };
1096        if !matches!(
1097            kind.as_str(),
1098            "table"
1099                | "column"
1100                | "index"
1101                | "view"
1102                | "sequence"
1103                | "schema"
1104                | "type"
1105                | "database"
1106                | "function"
1107        ) {
1108            self.consume_until_statement_boundary();
1109            return Ok(Statement::Empty);
1110        }
1111        self.advance(); // the kind keyword
1112        // The object name. ⚠️ `expect_ident_like` strips a leading
1113        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1114        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1115        // `c`. Read the dotted parts from raw tokens instead, then let a
1116        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1117        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1118        loop {
1119            match self.advance() {
1120                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1121                other if unreserved_keyword_text(&other).is_some() => {
1122                    parts.push(unreserved_keyword_text(&other).unwrap());
1123                }
1124                other => {
1125                    return Err(ParseError {
1126                        message: alloc::format!("expected identifier, got {other:?}"),
1127                        token_pos: self.consumed_pos(),
1128                    });
1129                }
1130            }
1131            if matches!(self.peek(), Token::Dot) {
1132                self.advance();
1133            } else {
1134                break;
1135            }
1136        }
1137        // COLUMN wants `table.column`; every other kind wants a bare name.
1138        let want = if kind == "column" { 2 } else { 1 };
1139        while parts.len() > want {
1140            parts.remove(0);
1141        }
1142        let name = parts.join(".");
1143        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1144        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1145        // error here — a dump carrying one function comment failed to
1146        // restore. The list is consumed (the comment store keys by name;
1147        // overload-precise comments are the function-predicate follow-up).
1148        if matches!(self.peek(), Token::LParen)
1149            && matches!(
1150                kind.as_str(),
1151                "function" | "procedure" | "aggregate" | "routine"
1152            )
1153        {
1154            let mut depth = 0usize;
1155            loop {
1156                match self.advance() {
1157                    Token::LParen => depth += 1,
1158                    Token::RParen => {
1159                        depth -= 1;
1160                        if depth == 0 {
1161                            break;
1162                        }
1163                    }
1164                    Token::Eof => {
1165                        return Err(self.err(alloc::string::String::from(
1166                            "unterminated argument list in COMMENT ON",
1167                        )));
1168                    }
1169                    _ => {}
1170                }
1171            }
1172        }
1173        // `IS`
1174        if !matches!(self.peek(), Token::Is) {
1175            self.expect_keyword_ident("is")?;
1176        } else {
1177            self.advance();
1178        }
1179        let comment = match self.peek() {
1180            Token::Null => {
1181                self.advance();
1182                None
1183            }
1184            _ => Some(self.expect_string_literal()?),
1185        };
1186        Ok(Statement::CommentOn {
1187            kind,
1188            name,
1189            comment,
1190        })
1191    }
1192
1193    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1194    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1195    /// [CASCADE|RESTRICT]`.
1196    ///
1197    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1198    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1199    /// and the no-ON `GRANT role TO role` membership form — parses into
1200    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1201    /// on them still restores.
1202    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1203        self.advance(); // GRANT / REVOKE
1204        // REVOKE's optional `GRANT OPTION FOR` prefix.
1205        let mut grant_option = false;
1206        if !grant
1207            && self.peek_keyword_ident("grant")
1208            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1209        {
1210            self.advance(); // GRANT
1211            self.advance(); // OPTION
1212            self.expect_keyword_ident("for")?;
1213            grant_option = true;
1214        }
1215        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1216        // words each with an optional COLUMN list.
1217        let mut privileges: Vec<GrantPriv> = Vec::new();
1218        if matches!(self.peek(), Token::All) {
1219            self.advance();
1220            if self.peek_keyword_ident("privileges") {
1221                self.advance();
1222            }
1223            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1224            // column only.
1225            if matches!(self.peek(), Token::LParen) {
1226                let columns = self.parse_grant_column_list()?;
1227                privileges.push(GrantPriv {
1228                    word: "ALL".into(),
1229                    columns,
1230                });
1231            }
1232        } else {
1233            loop {
1234                // SELECT and INSERT lex as reserved tokens, so they never
1235                // reach `expect_ident_like` as plain idents; the rest
1236                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1237                // MAINTAIN) are ordinary identifiers.
1238                let w = match self.peek() {
1239                    Token::Select => {
1240                        self.advance();
1241                        "SELECT".to_string()
1242                    }
1243                    Token::Insert => {
1244                        self.advance();
1245                        "INSERT".to_string()
1246                    }
1247                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1248                    // schema / database, and it lexes as a reserved token.
1249                    Token::Create => {
1250                        self.advance();
1251                        "CREATE".to_string()
1252                    }
1253                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1254                    // alice`) these "privilege words" are ROLE NAMES, and a
1255                    // role name is case-sensitive. `priv_from_word` folds case
1256                    // itself when they really are privileges.
1257                    _ => self.expect_ident_like()?,
1258                };
1259                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1260                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1261                let columns = if matches!(self.peek(), Token::LParen) {
1262                    self.parse_grant_column_list()?
1263                } else {
1264                    Vec::new()
1265                };
1266                privileges.push(GrantPriv { word: w, columns });
1267                if matches!(self.peek(), Token::Comma) {
1268                    self.advance();
1269                } else {
1270                    break;
1271                }
1272            }
1273        }
1274        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1275        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1276        if !matches!(self.peek(), Token::On) {
1277            let roles: Vec<String> = core::mem::take(&mut privileges)
1278                .into_iter()
1279                .map(|p| p.word)
1280                .collect();
1281            let grantees = self.parse_grantee_list(grant)?;
1282            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1283            // no admin-option layer: a member cannot re-grant).
1284            self.consume_until_statement_boundary();
1285            return Ok(finish_grant(
1286                grant,
1287                GrantStatement {
1288                    privileges: Vec::new(),
1289                    object: GrantObject::Roles(roles),
1290                    grantees,
1291                    grant_option,
1292                },
1293            ));
1294        }
1295        self.advance(); // ON
1296        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1297        // the enforced case; anything else parses and no-ops.
1298        let mut class = "TABLE";
1299        match self.peek() {
1300            Token::Table => {
1301                self.advance();
1302            }
1303            Token::All => {
1304                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1305                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1306                // IN SCHEMA` stay no-ops and keep their own object class.
1307                self.advance(); // ALL
1308                let kind = match self.peek() {
1309                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1310                    // TABLES has its own token (SHOW TABLES owns it).
1311                    Token::Tables | Token::Table => "tables".to_string(),
1312                    _ => String::new(),
1313                };
1314                if !kind.is_empty() {
1315                    self.advance();
1316                }
1317                // `IN SCHEMA <name>`
1318                if matches!(self.peek(), Token::In) {
1319                    self.advance();
1320                    if self.peek_keyword_ident("schema") {
1321                        self.advance();
1322                        let _schema = self.expect_ident_like()?;
1323                    }
1324                }
1325                if kind != "tables" {
1326                    self.consume_until_statement_boundary();
1327                    return Ok(finish_grant(
1328                        grant,
1329                        GrantStatement {
1330                            privileges,
1331                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1332                            grantees: Vec::new(),
1333                            grant_option,
1334                        },
1335                    ));
1336                }
1337                let grantees = self.parse_grantee_list(grant)?;
1338                self.consume_until_statement_boundary();
1339                return Ok(finish_grant(
1340                    grant,
1341                    GrantStatement {
1342                        privileges,
1343                        object: GrantObject::AllTablesInSchema,
1344                        grantees,
1345                        grant_option,
1346                    },
1347                ));
1348            }
1349            Token::Ident(w) | Token::QuotedIdent(w) => {
1350                let lc = w.to_ascii_lowercase();
1351                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1352                // real objects with real ACLs now.
1353                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1354                    self.advance();
1355                    let mut names: Vec<String> = Vec::new();
1356                    loop {
1357                        let mut parts: Vec<String> = Vec::new();
1358                        loop {
1359                            parts.push(self.expect_ident_like()?);
1360                            if matches!(self.peek(), Token::Dot) {
1361                                self.advance();
1362                            } else {
1363                                break;
1364                            }
1365                        }
1366                        names.push(parts.pop().expect("at least one part"));
1367                        if matches!(self.peek(), Token::Comma) {
1368                            self.advance();
1369                        } else {
1370                            break;
1371                        }
1372                    }
1373                    let grantees = self.parse_grantee_list(grant)?;
1374                    let mut grant_option = grant_option;
1375                    if grant && self.peek_keyword_ident("with") {
1376                        self.advance();
1377                        self.expect_keyword_ident("grant")?;
1378                        self.expect_keyword_ident("option")?;
1379                        grant_option = true;
1380                    }
1381                    self.consume_until_statement_boundary();
1382                    let object = match lc.as_str() {
1383                        "sequence" => GrantObject::Sequences(names),
1384                        "schema" => GrantObject::Schemas(names),
1385                        _ => GrantObject::Databases(names),
1386                    };
1387                    return Ok(finish_grant(
1388                        grant,
1389                        GrantStatement {
1390                            privileges,
1391                            object,
1392                            grantees,
1393                            grant_option,
1394                        },
1395                    ));
1396                }
1397                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1398                // keys functions by NAME, so the argument list parses and is
1399                // dropped (an overload set shares one ACL — recorded residual).
1400                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1401                    self.advance();
1402                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1403                    loop {
1404                        let mut parts: Vec<String> = Vec::new();
1405                        loop {
1406                            parts.push(self.expect_ident_like()?);
1407                            if matches!(self.peek(), Token::Dot) {
1408                                self.advance();
1409                            } else {
1410                                break;
1411                            }
1412                        }
1413                        let fname = parts.pop().expect("at least one part");
1414                        // v7.39 (read01 round 62) — the signature picks the
1415                        // overload, so it is captured.
1416                        let sig = if matches!(self.peek(), Token::LParen) {
1417                            Some(self.parse_function_signature_types()?)
1418                        } else {
1419                            None
1420                        };
1421                        names.push((fname, sig));
1422                        if matches!(self.peek(), Token::Comma) {
1423                            self.advance();
1424                        } else {
1425                            break;
1426                        }
1427                    }
1428                    let grantees = self.parse_grantee_list(grant)?;
1429                    self.consume_until_statement_boundary();
1430                    return Ok(finish_grant(
1431                        grant,
1432                        GrantStatement {
1433                            privileges,
1434                            object: GrantObject::Functions(names),
1435                            grantees,
1436                            grant_option,
1437                        },
1438                    ));
1439                }
1440                if matches!(
1441                    lc.as_str(),
1442                    "type"
1443                        | "domain"
1444                        | "language"
1445                        | "tablespace"
1446                        | "large"
1447                        | "foreign"
1448                        | "parameter"
1449                ) {
1450                    self.consume_until_statement_boundary();
1451                    return Ok(finish_grant(
1452                        grant,
1453                        GrantStatement {
1454                            privileges,
1455                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1456                            grantees: Vec::new(),
1457                            grant_option,
1458                        },
1459                    ));
1460                }
1461                class = "TABLE";
1462            }
1463            _ => {}
1464        }
1465        let _ = class;
1466        // The table list. Schema-qualified names drop their qualifier (SPG is
1467        // single-schema) — but read the dotted parts from raw tokens, since
1468        // `expect_ident_like` would silently swallow the leading part.
1469        let mut tables: Vec<String> = Vec::new();
1470        loop {
1471            let mut parts: Vec<String> = Vec::new();
1472            loop {
1473                parts.push(self.expect_ident_like()?);
1474                if matches!(self.peek(), Token::Dot) {
1475                    self.advance();
1476                } else {
1477                    break;
1478                }
1479            }
1480            tables.push(parts.pop().expect("at least one part"));
1481            if matches!(self.peek(), Token::Comma) {
1482                self.advance();
1483            } else {
1484                break;
1485            }
1486        }
1487        let grantees = self.parse_grantee_list(grant)?;
1488        if grant && self.peek_keyword_ident("with") {
1489            self.advance();
1490            self.expect_keyword_ident("grant")?;
1491            self.expect_keyword_ident("option")?;
1492            grant_option = true;
1493        }
1494        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1495        // to cascade to (no re-granting), so both are accepted and ignored.
1496        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1497            self.advance();
1498        }
1499        Ok(finish_grant(
1500            grant,
1501            GrantStatement {
1502                privileges,
1503                object: GrantObject::Tables(tables),
1504                grantees,
1505                grant_option,
1506            },
1507        ))
1508    }
1509
1510    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1511    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1512    /// words; the caller normalises them into a signature key.
1513    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1514        self.advance(); // (
1515        let mut types: Vec<String> = Vec::new();
1516        if matches!(self.peek(), Token::RParen) {
1517            self.advance();
1518            return Ok(types);
1519        }
1520        loop {
1521            // Collect the words of one argument up to a comma / close paren.
1522            let mut words: Vec<String> = Vec::new();
1523            loop {
1524                match self.peek() {
1525                    Token::Comma | Token::RParen | Token::Eof => break,
1526                    _ => {}
1527                }
1528                let tok = self.advance();
1529                match tok {
1530                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1531                    other => {
1532                        if let Some(w) = unreserved_keyword_text(&other) {
1533                            words.push(w);
1534                        }
1535                    }
1536                }
1537            }
1538            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1539            // themselves several words (`double precision`, `character
1540            // varying`, `timestamp with time zone`), so "two words means the
1541            // first is a parameter name" reads the type off `f(double
1542            // precision)` as `precision`. v7.39 (round 282): recognise the
1543            // multi-word spellings first — a leading word that STARTS one of
1544            // them is part of the type, not a name.
1545            let joined = words.join(" ");
1546            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1547                joined
1548            } else if words.len() >= 2 {
1549                words[1..].join(" ")
1550            } else {
1551                words.first().cloned().unwrap_or_default()
1552            };
1553            types.push(ty);
1554            if matches!(self.peek(), Token::Comma) {
1555                self.advance();
1556            } else {
1557                break;
1558            }
1559        }
1560        if matches!(self.peek(), Token::RParen) {
1561            self.advance();
1562        }
1563        Ok(types)
1564    }
1565
1566    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1567    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1568        self.advance(); // (
1569        let mut cols = Vec::new();
1570        loop {
1571            cols.push(self.expect_ident_like()?);
1572            if matches!(self.peek(), Token::Comma) {
1573                self.advance();
1574            } else {
1575                break;
1576            }
1577        }
1578        if !matches!(self.peek(), Token::RParen) {
1579            return Err(self.err(alloc::format!(
1580                "expected ')' to close the column list, got {:?}",
1581                self.peek()
1582            )));
1583        }
1584        self.advance(); // )
1585        Ok(cols)
1586    }
1587
1588    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1589    /// PUBLIC.
1590    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1591        if grant {
1592            if matches!(self.peek(), Token::To) {
1593                self.advance();
1594            } else {
1595                self.expect_keyword_ident("to")?;
1596            }
1597        } else if matches!(self.peek(), Token::From) {
1598            self.advance();
1599        } else {
1600            self.expect_keyword_ident("from")?;
1601        }
1602        let mut grantees: Vec<String> = Vec::new();
1603        loop {
1604            // `GROUP name` is the legacy spelling of a plain role name.
1605            if self.peek_keyword_ident("group") {
1606                self.advance();
1607            }
1608            if self.peek_keyword_ident("public") {
1609                self.advance();
1610                grantees.push(String::new()); // PUBLIC
1611            } else {
1612                grantees.push(self.expect_ident_like()?);
1613            }
1614            if matches!(self.peek(), Token::Comma) {
1615                self.advance();
1616            } else {
1617                break;
1618            }
1619        }
1620        Ok(grantees)
1621    }
1622
1623    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1624    /// The body keeps its `$N` placeholders; substitution happens at
1625    /// EXECUTE. The declared types are recorded for
1626    /// `pg_prepared_statements.parameter_types` but are not enforced —
1627    /// PG infers when the list is omitted, and SPG resolves the values
1628    /// at substitution time either way.
1629    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1630        let start = self.pos;
1631        self.advance(); // PREPARE
1632        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1633        // different statement that happens to share the keyword. PG
1634        // ships with `max_prepared_transactions = 0` and reports it
1635        // this way; SPG has no prepared-transaction registry, so the
1636        // same wording is the accurate answer rather than a dodge.
1637        // Round 277 turned this from a silent no-op into a confusing
1638        // "expected AS in PREPARE" parse error.
1639        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1640            self.advance();
1641            let gid = match self.advance() {
1642                Token::String(g) => g,
1643                other => {
1644                    return Err(self.err(alloc::format!(
1645                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1646                    )));
1647                }
1648            };
1649            return Ok(Statement::PrepareTransaction(gid));
1650        }
1651        let name = self.expect_ident_like()?;
1652        let mut param_types = Vec::new();
1653        if matches!(self.peek(), Token::LParen) {
1654            self.advance();
1655            loop {
1656                let mut ty = self.expect_ident_like()?;
1657                // A parameterised type name (`numeric(10,2)`,
1658                // `varchar(20)`) keeps its argument list in the text.
1659                if matches!(self.peek(), Token::LParen) {
1660                    let mut depth = 0usize;
1661                    let mut buf = String::from("(");
1662                    loop {
1663                        match self.advance() {
1664                            Token::LParen => {
1665                                depth += 1;
1666                                if depth > 1 {
1667                                    buf.push('(');
1668                                }
1669                            }
1670                            Token::RParen => {
1671                                depth -= 1;
1672                                buf.push(')');
1673                                if depth == 0 {
1674                                    break;
1675                                }
1676                            }
1677                            Token::Comma => buf.push(','),
1678                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1679                            Token::Eof => break,
1680                            _ => {}
1681                        }
1682                    }
1683                    ty.push_str(&buf);
1684                }
1685                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1686                // position, same family as the parameter list above.
1687                let array_suffix = self.consume_array_suffix();
1688                ty.push_str(&array_suffix);
1689                param_types.push(ty);
1690                match self.peek() {
1691                    Token::Comma => {
1692                        self.advance();
1693                    }
1694                    Token::RParen => {
1695                        self.advance();
1696                        break;
1697                    }
1698                    other => {
1699                        return Err(self.err(alloc::format!(
1700                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1701                        )));
1702                    }
1703                }
1704            }
1705        }
1706        if !matches!(self.peek(), Token::As) {
1707            return Err(self.err(alloc::format!(
1708                "expected AS in PREPARE, got {:?}",
1709                self.peek()
1710            )));
1711        }
1712        self.advance();
1713        let body = self.parse_one_statement()?;
1714        // The Parser holds tokens, not the source text, so the
1715        // statement PG reports in `pg_prepared_statements.statement`
1716        // is rebuilt from the AST rather than sliced from the input.
1717        let _ = start;
1718        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1719        if !param_types.is_empty() {
1720            source.push_str(" (");
1721            source.push_str(&param_types.join(", "));
1722            source.push(')');
1723        }
1724        source.push_str(" AS ");
1725        source.push_str(&alloc::format!("{body}"));
1726        Ok(Statement::Prepare {
1727            name,
1728            param_types,
1729            body: alloc::boxed::Box::new(body),
1730            source,
1731        })
1732    }
1733
1734    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1735    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1736        self.advance(); // EXECUTE
1737        let name = self.expect_ident_like()?;
1738        let mut args = Vec::new();
1739        if matches!(self.peek(), Token::LParen) {
1740            self.advance();
1741            if matches!(self.peek(), Token::RParen) {
1742                self.advance();
1743            } else {
1744                loop {
1745                    args.push(self.parse_expr(0)?);
1746                    match self.advance() {
1747                        Token::Comma => {}
1748                        Token::RParen => break,
1749                        other => {
1750                            return Err(self.err(alloc::format!(
1751                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1752                            )));
1753                        }
1754                    }
1755                }
1756            }
1757        }
1758        Ok(Statement::Execute { name, args })
1759    }
1760
1761    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1762    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1763    /// procedure catalog yet, so this reports PG's not-found error
1764    /// (with its HINT) rather than pretending the call ran.
1765    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1766    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1767    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1768        self.advance(); // DISCARD
1769        let target = match self.advance() {
1770            Token::All => DiscardTarget::All,
1771            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1772                "all" => DiscardTarget::All,
1773                "plans" => DiscardTarget::Plans,
1774                "sequences" => DiscardTarget::Sequences,
1775                "temp" | "temporary" => DiscardTarget::Temp,
1776                other => {
1777                    return Err(self.err(format!(
1778                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1779                    )));
1780                }
1781            },
1782            other => {
1783                return Err(self.err(format!(
1784                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785                )));
1786            }
1787        };
1788        Ok(Statement::Discard(target))
1789    }
1790
1791    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1792    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1793    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1794    /// aggressively the server interrupts, which SPG does not distinguish.
1795    /// Bare `KILL <id>` means CONNECTION.
1796    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1797        self.advance(); // KILL
1798        let mut query_only = false;
1799        loop {
1800            // CONNECTION is a reserved keyword token (it also opens
1801            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1802            // `Token::Connection` rather than a bare ident.
1803            if matches!(self.peek(), Token::Connection) {
1804                self.advance();
1805                break;
1806            }
1807            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1808                break;
1809            };
1810            match w.to_ascii_lowercase().as_str() {
1811                "hard" | "soft" => {
1812                    self.advance();
1813                }
1814                "query" => {
1815                    self.advance();
1816                    query_only = true;
1817                    break;
1818                }
1819                _ => break,
1820            }
1821        }
1822        let id = self.parse_expr(0)?;
1823        Ok(Statement::Kill {
1824            query_only,
1825            id: Box::new(id),
1826        })
1827    }
1828
1829    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1830        self.advance(); // CALL
1831        let name = self.expect_ident_like()?;
1832        self.consume_until_statement_boundary();
1833        Ok(Statement::Call(name))
1834    }
1835
1836    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1837        self.advance(); // DEALLOCATE
1838        // PG accepts an optional noise `PREPARE` keyword here.
1839        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1840            self.advance();
1841        }
1842        if matches!(self.peek(), Token::All) {
1843            self.advance();
1844            return Ok(Statement::Deallocate(None));
1845        }
1846        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1847            self.advance();
1848            return Ok(Statement::Deallocate(None));
1849        }
1850        let name = self.expect_ident_like()?;
1851        Ok(Statement::Deallocate(Some(name)))
1852    }
1853
1854    fn consume_until_statement_boundary(&mut self) {
1855        loop {
1856            match self.peek() {
1857                Token::Semicolon | Token::Eof => return,
1858                _ => self.advance(),
1859            };
1860        }
1861    }
1862
1863    /// v7.22 (round-13 T2) — consume to the statement boundary like
1864    /// `consume_until_statement_boundary`, but pick out the sequence
1865    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1866    /// columns) or the first string literal (`nextval('<seq>')`).
1867    /// Schema qualifiers and `::regclass` casts are stripped.
1868    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1869        let mut seq: Option<String> = None;
1870        let mut after_sequence_kw = false;
1871        let mut after_name_kw = false;
1872        loop {
1873            match self.peek().clone() {
1874                Token::Semicolon | Token::Eof => break,
1875                Token::Ident(s) | Token::QuotedIdent(s) => {
1876                    if after_name_kw && seq.is_none() {
1877                        self.advance();
1878                        let mut name = s;
1879                        // `SEQUENCE NAME public.groups_id_seq` — keep
1880                        // the bare name, drop qualifiers.
1881                        while matches!(self.peek(), Token::Dot) {
1882                            self.advance();
1883                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1884                                name = n;
1885                            }
1886                        }
1887                        seq = Some(name);
1888                        after_name_kw = false;
1889                        continue;
1890                    }
1891                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1892                        after_name_kw = true;
1893                        after_sequence_kw = false;
1894                    } else {
1895                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1896                    }
1897                    self.advance();
1898                }
1899                Token::String(s) => {
1900                    if seq.is_none() {
1901                        // `nextval('public.groups_id_seq'::regclass)`
1902                        let bare = s
1903                            .rsplit_once('.')
1904                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1905                        seq = Some(bare);
1906                    }
1907                    self.advance();
1908                }
1909                _ => {
1910                    after_sequence_kw = false;
1911                    after_name_kw = false;
1912                    self.advance();
1913                }
1914            }
1915        }
1916        seq
1917    }
1918
1919    /// v7.39 (round 621) — is the next token the keyword `BY`?
1920    ///
1921    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1922    /// column, table and alias name — and SPG lexed it into a dedicated
1923    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1924    /// two-letter keywords the lexer knew, this was the only one PG leaves
1925    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1926    ///
1927    /// The token is gone; the three clauses that own the word — GROUP BY,
1928    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1929    /// ask this instead. Adding it to the unreserved-identifier table was not
1930    /// enough on its own: identifier positions that match the token shape
1931    /// directly (an index's column list, a table alias) never consult that
1932    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1933    /// Not lexing it as a keyword closes the whole class rather than the two
1934    /// positions that happened to be noticed.
1935    fn peek_is_by(&self) -> bool {
1936        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1937    }
1938
1939    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1940    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1941    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1942    fn consume_drop_behaviour(&mut self) {
1943        if matches!(
1944            self.peek(),
1945            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1946        ) {
1947            self.advance();
1948        }
1949    }
1950
1951    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1952        let first = match self.advance() {
1953            Token::Ident(s) | Token::QuotedIdent(s) => s,
1954            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1955            // per PG's `pg_get_keywords()` classification. SPG tokenizes
1956            // these as named variants for parsing leverage in the
1957            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1958            // `BEGIN`, etc.), but they MUST still be usable as table /
1959            // column / alias names in DDL+DML. Sentori migrations like
1960            // 0001_init.sql ship `release TEXT NOT NULL` in the events
1961            // table — the `events.release` column carries the release
1962            // identifier string. Pre-T4 this triggered "expected
1963            // identifier, got Release" and blocked every drop-in user
1964            // whose schema had a column / alias with one of these names.
1965            other if unreserved_keyword_text(&other).is_some() => {
1966                unreserved_keyword_text(&other).unwrap()
1967            }
1968            other => {
1969                return Err(ParseError {
1970                    message: format!("expected identifier, got {other:?}"),
1971                    token_pos: self.consumed_pos(),
1972                });
1973            }
1974        };
1975        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1976        // qualify every name with `public.` (and pg_catalog.* for
1977        // functions); SPG is single-schema so we discard the
1978        // prefix and return only the trailing ident. Same shape
1979        // also handles MySQL `db.tbl` cross-database refs (SPG
1980        // ignores the db part).
1981        if matches!(self.peek(), Token::Dot) {
1982            self.advance();
1983            match self.advance() {
1984                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1985                other if unreserved_keyword_text(&other).is_some() => {
1986                    return Ok(unreserved_keyword_text(&other).unwrap());
1987                }
1988                other => {
1989                    return Err(ParseError {
1990                        message: format!("expected identifier after '{first}.', got {other:?}"),
1991                        token_pos: self.consumed_pos(),
1992                    });
1993                }
1994            }
1995        }
1996        Ok(first)
1997    }
1998
1999    #[allow(clippy::too_many_lines)]
2000    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2001        // v7.14.0 — empty / comment-only / semicolon-only input
2002        // (after the lexer strips line + block + MySQL
2003        // conditional comments) lands as Statement::Empty.
2004        // pg_dump and mysqldump emit several wrappers that
2005        // collapse to nothing after stripping (`/*!40101 SET …
2006        // */;`, blank lines between statements); the engine
2007        // returns CommandOk no-op so the dump loads cleanly.
2008        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2009            return Ok(Statement::Empty);
2010        }
2011        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2012        // catalog / metadata DDL that has no behavioural effect
2013        // on SPG's single-schema, single-database, single-user
2014        // model. Consume the whole statement up to the next
2015        // semicolon / EOF and return Empty. This is broader than
2016        // the per-keyword DROP / SET / COMMENT arms but lets the
2017        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2018        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2019        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2020        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2021            let lc = s.to_ascii_lowercase();
2022            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2023            if lc == "comment" {
2024                return self.parse_comment_on();
2025            }
2026            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2027            if lc == "grant" || lc == "revoke" {
2028                return self.parse_grant_or_revoke(lc == "grant");
2029            }
2030            // v7.39 (round 277) — the SQL-level prepared-statement
2031            // surface is REAL now. It used to be accepted and dropped
2032            // on the theory that "real execution still happens via the
2033            // extended-query flow" — true only for a driver that uses
2034            // that flow; a plain SQL PREPARE / EXECUTE returned no
2035            // rows at all.
2036            if lc == "prepare" {
2037                return self.parse_prepare();
2038            }
2039            if lc == "execute" {
2040                return self.parse_execute();
2041            }
2042            if lc == "deallocate" {
2043                return self.parse_deallocate();
2044            }
2045            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2046            // accepted and dropped, so an application's stored-procedure
2047            // invocation reported success and did nothing. SPG has no
2048            // procedure catalog, so every CALL names a procedure that
2049            // does not exist — which is exactly what PG says.
2050            if lc == "call" {
2051                return self.parse_call();
2052            }
2053            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2054            // names one connection and acts on it.
2055            if lc == "kill" {
2056                return self.parse_kill();
2057            }
2058            if lc == "discard" {
2059                return self.parse_discard();
2060            }
2061            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2062            // Still performs nothing; the roles are carried out so a name
2063            // that does not exist is refused, as PG18 refuses it.
2064            if lc == "reassign" {
2065                self.advance();
2066                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2067                    self.advance();
2068                }
2069                if self.peek_is_by() {
2070                    self.advance();
2071                }
2072                // Only the roles BEFORE the TO are the ones that must
2073                // exist — `TO` names the new owner, which PG checks as
2074                // well, so both lists are collected.
2075                let mut names = self.take_comma_separated_names();
2076                if matches!(self.peek(), Token::To) {
2077                    self.advance();
2078                    names.extend(self.take_comma_separated_names());
2079                }
2080                self.consume_until_statement_boundary();
2081                return Ok(Statement::ValidateOnly {
2082                    kind: crate::ast::ValidateOnlyKind::RoleName,
2083                    names,
2084                });
2085            }
2086            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2087            // unconditionally with `no security label providers have been
2088            // loaded`, whatever object it names, because none is loaded.
2089            // SPG has none either; accepting it told the caller a label had
2090            // been applied when nothing anywhere records one.
2091            if lc == "security" {
2092                self.consume_until_statement_boundary();
2093                return Ok(Statement::ValidateOnly {
2094                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2095                    names: Vec::new(),
2096                });
2097            }
2098            if is_dump_noise_statement(&lc) {
2099                self.consume_until_statement_boundary();
2100                return Ok(Statement::Empty);
2101            }
2102        }
2103        match self.peek() {
2104            Token::Select => self.parse_select_stmt(),
2105            // v7.37.17 (17.6 siblings) — a statement opening with a
2106            // parenthesized query group: `(SELECT … UNION …)
2107            // INTERSECT …`. parse_bare_select's group arm consumes
2108            // the parens; the select parser handles the outer chain
2109            // and tail.
2110            Token::LParen
2111                if matches!(
2112                    self.tokens.get(self.pos + 1),
2113                    Some(Token::Select | Token::LParen | Token::Values)
2114                ) =>
2115            {
2116                self.parse_select_stmt()
2117            }
2118            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2119            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2120            // Lowers to the same UNION ALL chain the FROM-position
2121            // form uses, then reuses the shared SELECT tail.
2122            Token::Values => {
2123                self.advance(); // VALUES
2124                let mut head = self.parse_values_rows_body()?;
2125                self.parse_select_tail_into(&mut head)?;
2126                Ok(Statement::Select(head))
2127            }
2128            // SQL-standard `TABLE name` shorthand for
2129            // `SELECT * FROM name` — pg_dump never emits it, but
2130            // psql users and PG docs use it constantly. Set-op
2131            // chains and the ORDER BY/LIMIT tail compose like any
2132            // SELECT head.
2133            Token::Table
2134                if matches!(
2135                    self.tokens.get(self.pos + 1),
2136                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2137                ) =>
2138            {
2139                let mut head = self.parse_table_shorthand()?;
2140                self.parse_setop_chain_into(&mut head)?;
2141                self.parse_select_tail_into(&mut head)?;
2142                Ok(Statement::Select(head))
2143            }
2144            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2145            // body is a dollar-quoted plpgsql block (lexer already
2146            // collapsed `$$…$$` into a single Token::String).
2147            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2148            // real PlPgSqlBlock so the engine can EXECUTE it at
2149            // top level instead of silently swallowing. Pre-
2150            // v7.16.2 the parser threw the body away and the
2151            // engine returned CommandOk for the entire DO; that
2152            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2153            // $$` into a SEV-1 silent no-op (the IF + the rename
2154            // were both invisible — mailrs's migrate-042 didn't
2155            // actually run). Now the body parses + executes;
2156            // EmbeddedSql inside the block runs immediately
2157            // against the engine (not deferred — we're at top
2158            // level, not inside a trigger row-write loop).
2159            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2160                self.advance();
2161                let body_text = match self.advance() {
2162                    Token::String(s) => s,
2163                    other => {
2164                        return Err(self.err(alloc::format!(
2165                            "expected dollar-quoted body after DO, got {other:?}"
2166                        )));
2167                    }
2168                };
2169                // Optional `LANGUAGE <name>` trailer (idents only).
2170                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2171                    self.advance();
2172                    let _ = self.expect_ident_like()?;
2173                }
2174                // Parse the body — same shape CREATE FUNCTION
2175                // uses for trigger function bodies. If the body
2176                // doesn't parse cleanly we surface the error
2177                // (better than silent no-op).
2178                let block = parse_plpgsql_body(&body_text)?;
2179                Ok(Statement::DoBlock(block))
2180            }
2181            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2182            // WITH isn't a reserved token in our lexer — comes through
2183            // as `Token::Ident("with")` (case-insensitive).
2184            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2185                self.advance();
2186                self.parse_with_cte_then_select()
2187            }
2188            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2189            // an identifier — not a reserved keyword.
2190            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2191                self.advance();
2192                let mut analyze = false;
2193                let mut suggest = false;
2194                let mut costs_off = false;
2195                let mut buffers = false;
2196                let mut timing_off = false;
2197                let mut settings = false;
2198                let mut wal = false;
2199                let mut summary_off = false;
2200                let mut format = crate::ast::ExplainFormat::Text;
2201                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2202                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2203                // options are comma-separated. Booleans default to ON
2204                // when the value token is omitted (matches PG).
2205                if matches!(self.peek(), Token::LParen) {
2206                    self.advance();
2207                    loop {
2208                        let opt = match self.peek().clone() {
2209                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2210                            other => {
2211                                return Err(self.err(format!(
2212                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2213                                )));
2214                            }
2215                        };
2216                        self.advance();
2217                        if opt.eq_ignore_ascii_case("suggest") {
2218                            suggest = true;
2219                            // SUGGEST takes no explicit value today.
2220                        } else if opt.eq_ignore_ascii_case("costs") {
2221                            // PG syntax: `COSTS [ON | OFF]`. Default
2222                            // when value omitted is ON, so plain
2223                            // `COSTS` is a no-op. `COSTS OFF` flips.
2224                            // `ON` lexes to `Token::On` (reserved
2225                            // keyword in JOIN ... ON contexts); accept
2226                            // it alongside the bare Ident form so the
2227                            // grammar matches PG verbatim.
2228                            let value = match self.peek().clone() {
2229                                Token::On => {
2230                                    self.advance();
2231                                    true
2232                                }
2233                                Token::Ident(v) | Token::QuotedIdent(v)
2234                                    if v.eq_ignore_ascii_case("off") =>
2235                                {
2236                                    self.advance();
2237                                    false
2238                                }
2239                                Token::Ident(v) | Token::QuotedIdent(v)
2240                                    if v.eq_ignore_ascii_case("true") =>
2241                                {
2242                                    self.advance();
2243                                    true
2244                                }
2245                                _ => true,
2246                            };
2247                            costs_off = !value;
2248                        } else if opt.eq_ignore_ascii_case("analyze")
2249                            || opt.eq_ignore_ascii_case("analyse")
2250                        {
2251                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2252                            // Same default-ON rule as ANALYZE keyword form.
2253                            let value = match self.peek().clone() {
2254                                Token::On => {
2255                                    self.advance();
2256                                    true
2257                                }
2258                                Token::Ident(v) | Token::QuotedIdent(v)
2259                                    if v.eq_ignore_ascii_case("off") =>
2260                                {
2261                                    self.advance();
2262                                    false
2263                                }
2264                                Token::Ident(v) | Token::QuotedIdent(v)
2265                                    if v.eq_ignore_ascii_case("true") =>
2266                                {
2267                                    self.advance();
2268                                    true
2269                                }
2270                                _ => true,
2271                            };
2272                            analyze = value;
2273                        } else if opt.eq_ignore_ascii_case("buffers") {
2274                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2275                            let value = match self.peek().clone() {
2276                                Token::On => {
2277                                    self.advance();
2278                                    true
2279                                }
2280                                Token::Ident(v) | Token::QuotedIdent(v)
2281                                    if v.eq_ignore_ascii_case("off") =>
2282                                {
2283                                    self.advance();
2284                                    false
2285                                }
2286                                Token::Ident(v) | Token::QuotedIdent(v)
2287                                    if v.eq_ignore_ascii_case("true") =>
2288                                {
2289                                    self.advance();
2290                                    true
2291                                }
2292                                _ => true,
2293                            };
2294                            buffers = value;
2295                        } else if opt.eq_ignore_ascii_case("timing") {
2296                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2297                            // the measured wall-clock annotation.
2298                            let value = match self.peek().clone() {
2299                                Token::On => {
2300                                    self.advance();
2301                                    true
2302                                }
2303                                Token::Ident(v) | Token::QuotedIdent(v)
2304                                    if v.eq_ignore_ascii_case("off") =>
2305                                {
2306                                    self.advance();
2307                                    false
2308                                }
2309                                Token::Ident(v) | Token::QuotedIdent(v)
2310                                    if v.eq_ignore_ascii_case("true") =>
2311                                {
2312                                    self.advance();
2313                                    true
2314                                }
2315                                _ => true,
2316                            };
2317                            timing_off = !value;
2318                        } else if opt.eq_ignore_ascii_case("settings") {
2319                            settings = true;
2320                        } else if opt.eq_ignore_ascii_case("wal") {
2321                            wal = true;
2322                        } else if opt.eq_ignore_ascii_case("summary") {
2323                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2324                            // gates the trailing Planning/Execution Time
2325                            // lines now (was accept-and-no-op).
2326                            let value = match self.peek().clone() {
2327                                Token::On => {
2328                                    self.advance();
2329                                    true
2330                                }
2331                                Token::Ident(v) | Token::QuotedIdent(v)
2332                                    if v.eq_ignore_ascii_case("off") =>
2333                                {
2334                                    self.advance();
2335                                    false
2336                                }
2337                                Token::Ident(v) | Token::QuotedIdent(v)
2338                                    if v.eq_ignore_ascii_case("true") =>
2339                                {
2340                                    self.advance();
2341                                    true
2342                                }
2343                                _ => true,
2344                            };
2345                            summary_off = !value;
2346                        } else if opt.eq_ignore_ascii_case("verbose")
2347                            || opt.eq_ignore_ascii_case("format")
2348                        {
2349                            // v7.37.22 — accept-but-no-op the remaining
2350                            // PG options so EXPLAIN-using clients
2351                            // (pgAdmin / DataGrip) don't see syntax
2352                            // errors. FORMAT takes a value (text /
2353                            // json / yaml / xml); skip the next token
2354                            // if it's an ident.
2355                            if opt.eq_ignore_ascii_case("format") {
2356                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2357                                {
2358                                    self.advance();
2359                                    format = match v.to_ascii_lowercase().as_str() {
2360                                        "text" => crate::ast::ExplainFormat::Text,
2361                                        "json" => crate::ast::ExplainFormat::Json,
2362                                        "xml" => crate::ast::ExplainFormat::Xml,
2363                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2364                                        other => {
2365                                            return Err(self.err(format!(
2366                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2367                                                 supports text, json, xml, yaml"
2368                                            )));
2369                                        }
2370                                    };
2371                                }
2372                            } else {
2373                                // VERBOSE / SUMMARY take optional ON/OFF;
2374                                // consume if present.
2375                                if matches!(self.peek(), Token::On) {
2376                                    self.advance();
2377                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2378                                    self.peek().clone()
2379                                    && (v.eq_ignore_ascii_case("off")
2380                                        || v.eq_ignore_ascii_case("true"))
2381                                {
2382                                    self.advance();
2383                                    let _ = v;
2384                                }
2385                            }
2386                        } else {
2387                            return Err(self.err(format!(
2388                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2389                            )));
2390                        }
2391                        if matches!(self.peek(), Token::Comma) {
2392                            self.advance();
2393                            continue;
2394                        }
2395                        break;
2396                    }
2397                    if !matches!(self.peek(), Token::RParen) {
2398                        return Err(self.err(format!(
2399                            "expected ')' after EXPLAIN options, got {:?}",
2400                            self.peek()
2401                        )));
2402                    }
2403                    self.advance();
2404                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2405                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2406                {
2407                    self.advance();
2408                    analyze = true;
2409                }
2410                // v7.39 (round 224) — the body may open with WITH (CTEs);
2411                // route through the same CTE-then-SELECT path the top-level
2412                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2413                // too (PG explains INSERT / UPDATE / DELETE).
2414                let inner = match self.peek().clone() {
2415                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2416                        self.advance();
2417                        self.parse_with_cte_then_select()?
2418                    }
2419                    Token::Insert => self.parse_insert_stmt(false)?,
2420                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2421                        self.advance();
2422                        self.parse_update_after_keyword()?
2423                    }
2424                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2425                        self.advance();
2426                        self.parse_delete_after_keyword()?
2427                    }
2428                    _ => self.parse_select_stmt()?,
2429                };
2430                if !matches!(
2431                    inner,
2432                    Statement::Select(_)
2433                        | Statement::Insert(_)
2434                        | Statement::Update(_)
2435                        | Statement::Delete(_)
2436                ) {
2437                    return Err(self.err(format!(
2438                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2439                    )));
2440                }
2441                Ok(Statement::Explain(crate::ast::ExplainStatement {
2442                    analyze,
2443                    inner: Box::new(inner),
2444                    suggest,
2445                    costs_off,
2446                    buffers,
2447                    timing_off,
2448                    settings,
2449                    wal,
2450                    summary_off,
2451                    format,
2452                }))
2453            }
2454            Token::Create => self.parse_create_stmt(),
2455            Token::Insert => self.parse_insert_stmt(false),
2456            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2457            // spelling; route to the same handler. DESC is the
2458            // reserved ORDER BY token, so it gets its own arm.
2459            Token::Ident(s)
2460                if s.eq_ignore_ascii_case("describe")
2461                    && matches!(
2462                        self.tokens.get(self.pos + 1),
2463                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2464                    ) =>
2465            {
2466                self.advance();
2467                let table = self.expect_ident_like()?;
2468                Ok(Statement::ShowColumns(table))
2469            }
2470            Token::Desc
2471                if matches!(
2472                    self.tokens.get(self.pos + 1),
2473                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2474                ) =>
2475            {
2476                self.advance();
2477                let table = self.expect_ident_like()?;
2478                Ok(Statement::ShowColumns(table))
2479            }
2480            // `COPY table [(cols)] TO STDOUT` — the export half of
2481            // pg_dump's COPY pair (the FROM stdin half rides the
2482            // embed import path). Options need a format design and
2483            // error honestly.
2484            Token::Ident(s)
2485                if s.eq_ignore_ascii_case("copy")
2486                    && matches!(
2487                        self.tokens.get(self.pos + 1),
2488                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2489                    ) =>
2490            {
2491                self.advance(); // COPY
2492                let table = self.expect_ident_like()?;
2493                let columns = if matches!(self.peek(), Token::LParen) {
2494                    self.advance();
2495                    let mut cols = alloc::vec![self.expect_ident_like()?];
2496                    while matches!(self.peek(), Token::Comma) {
2497                        self.advance();
2498                        cols.push(self.expect_ident_like()?);
2499                    }
2500                    if !matches!(self.peek(), Token::RParen) {
2501                        return Err(self.err(format!(
2502                            "expected ')' after COPY column list, got {:?}",
2503                            self.peek()
2504                        )));
2505                    }
2506                    self.advance();
2507                    Some(cols)
2508                } else {
2509                    None
2510                };
2511                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2512                // endpoint. (FROM STDIN still rides the wire/import path —
2513                // its data arrives out of band.)
2514                if matches!(self.peek(), Token::From)
2515                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2516                {
2517                    self.advance(); // FROM
2518                    let Token::String(path) = self.advance() else {
2519                        unreachable!()
2520                    };
2521                    let options = self.parse_copy_to_options()?;
2522                    return Ok(Statement::CopyFromFile {
2523                        table,
2524                        columns,
2525                        path,
2526                        options,
2527                    });
2528                }
2529                if !matches!(self.peek(), Token::To) {
2530                    return Err(self.err(format!(
2531                        "COPY: only TO STDOUT is supported here (FROM stdin \
2532                         rides the import path); got {:?}",
2533                        self.peek()
2534                    )));
2535                }
2536                self.advance();
2537                if matches!(self.peek(), Token::String(_)) {
2538                    let Token::String(path) = self.advance() else { unreachable!() };
2539                    let options = self.parse_copy_to_options()?;
2540                    return Ok(Statement::CopyToFile {
2541                        table,
2542                        columns,
2543                        query: None,
2544                        path,
2545                        options,
2546                    });
2547                }
2548                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2549                    return Err(self.err(format!(
2550                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2551                        self.peek()
2552                    )));
2553                }
2554                self.advance();
2555                let options = self.parse_copy_to_options()?;
2556                Ok(Statement::CopyTo {
2557                    table,
2558                    columns,
2559                    query: None,
2560                    options,
2561                })
2562            }
2563            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2564            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2565            // result set is streamed in COPY format (PG's query form).
2566            Token::Ident(s)
2567                if s.eq_ignore_ascii_case("copy")
2568                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2569            {
2570                self.advance(); // COPY
2571                self.advance(); // (
2572                let query = self.parse_select_stmt()?;
2573                if !matches!(self.peek(), Token::RParen) {
2574                    return Err(self.err(format!(
2575                        "expected ')' after COPY query, got {:?}",
2576                        self.peek()
2577                    )));
2578                }
2579                self.advance(); // )
2580                if !matches!(self.peek(), Token::To) {
2581                    return Err(self.err(format!(
2582                        "COPY (query): only TO STDOUT is supported, got {:?}",
2583                        self.peek()
2584                    )));
2585                }
2586                self.advance();
2587                if matches!(self.peek(), Token::String(_)) {
2588                    let Token::String(path) = self.advance() else { unreachable!() };
2589                    let options = self.parse_copy_to_options()?;
2590                    return Ok(Statement::CopyToFile {
2591                        table: String::new(),
2592                        columns: None,
2593                        query: Some(alloc::boxed::Box::new(query)),
2594                        path,
2595                        options,
2596                    });
2597                }
2598                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2599                    return Err(self.err(format!(
2600                        "COPY (query): TO supports STDOUT only, got {:?}",
2601                        self.peek()
2602                    )));
2603                }
2604                self.advance();
2605                let options = self.parse_copy_to_options()?;
2606                Ok(Statement::CopyTo {
2607                    table: String::new(),
2608                    columns: None,
2609                    query: Some(alloc::boxed::Box::new(query)),
2610                    options,
2611                })
2612            }
2613            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2614            // Shares the INSERT body; the replace flag lowers it
2615            // onto ON CONFLICT DO UPDATE with an empty assignment
2616            // list (engine: replace the whole row).
2617            Token::Ident(s)
2618                if s.eq_ignore_ascii_case("replace")
2619                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2620            {
2621                self.parse_insert_stmt(true)
2622            }
2623            Token::Begin => {
2624                self.advance();
2625                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2626                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2627                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2628                // is consumed first, then the trailing modes — including the
2629                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2630                // WORK/TRANSACTION). The explicit level, when present, rides the
2631                // statement so `exec_begin` applies it for this transaction.
2632                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2633                {
2634                    self.advance();
2635                }
2636                let iso = self.parse_isolation_level_clauses()?;
2637                Ok(Statement::Begin(iso))
2638            }
2639            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2640            // for BEGIN. START is contextual in PG too; pattern-match
2641            // on the ident here. Iso clauses are parse-and-ignored,
2642            // same as BEGIN above.
2643            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2644                self.advance();
2645                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2646                {
2647                    return Err(self.err(alloc::format!(
2648                        "expected TRANSACTION after START, got {:?}",
2649                        self.peek()
2650                    )));
2651                }
2652                self.advance();
2653                let iso = self.parse_isolation_level_clauses()?;
2654                Ok(Statement::Begin(iso))
2655            }
2656            Token::Commit => {
2657                self.advance();
2658                // PG: `COMMIT [WORK | TRANSACTION]`.
2659                if let Token::Ident(w) = self.peek()
2660                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2661                {
2662                    self.advance();
2663                }
2664                Ok(Statement::Commit)
2665            }
2666            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2667            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2668            // every transaction with `END;` and the drop-in aborted on
2669            // it. Only reachable at statement start (CASE … END lives
2670            // inside expressions), so no ambiguity.
2671            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2672                self.advance();
2673                if let Token::Ident(w) = self.peek()
2674                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2675                {
2676                    self.advance();
2677                }
2678                Ok(Statement::Commit)
2679            }
2680            Token::Rollback => {
2681                self.advance();
2682                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2683                // savepoint without ending the transaction. Bare
2684                // `ROLLBACK` drops the whole TX.
2685                if matches!(self.peek(), Token::To) {
2686                    self.advance();
2687                    if matches!(self.peek(), Token::Savepoint) {
2688                        self.advance();
2689                    }
2690                    let name = self.expect_ident_like()?;
2691                    Ok(Statement::RollbackToSavepoint(name))
2692                } else {
2693                    Ok(Statement::Rollback)
2694                }
2695            }
2696            Token::Savepoint => {
2697                self.advance();
2698                let name = self.expect_ident_like()?;
2699                Ok(Statement::Savepoint(name))
2700            }
2701            Token::Release => {
2702                self.advance();
2703                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2704                // is optional in standard SQL.
2705                if matches!(self.peek(), Token::Savepoint) {
2706                    self.advance();
2707                }
2708                let name = self.expect_ident_like()?;
2709                Ok(Statement::ReleaseSavepoint(name))
2710            }
2711            Token::Show => {
2712                self.advance();
2713                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2714                // v6.1.2 promoted TABLES to a reserved keyword (for
2715                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2716                // arrives as `Token::Tables` rather than a bare ident.
2717                // USERS / COLUMNS remain bare idents.
2718                let target = match self.advance() {
2719                    Token::Tables => "tables".to_string(),
2720                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2721                    // keyword token; recognise it as the SHOW CREATE
2722                    // dispatch keyword too.
2723                    Token::Create => "create".to_string(),
2724                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2725                    // keyword too; let SHOW INDEX FROM parse.
2726                    Token::Index => "index".to_string(),
2727                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2728                    // reserved (used in aggregate function calls);
2729                    // recognise it here so the parser dispatches
2730                    // to ShowParameter("all") — the engine returns
2731                    // the curated parameter inventory.
2732                    Token::All => "all".to_string(),
2733                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2734                    other => {
2735                        return Err(self.err(format!(
2736                            "expected SHOW target, got {other:?}"
2737                        )));
2738                    }
2739                };
2740                match target.as_str() {
2741                    "tables" => Ok(Statement::ShowTables),
2742                    "users" => Ok(Statement::ShowUsers),
2743                    // v7.38 轴 4 — `SHOW transaction_isolation`
2744                    // returns the currently-selected isolation level.
2745                    "transaction_isolation" => Ok(Statement::ShowParameter(
2746                        "transaction_isolation".to_string(),
2747                    )),
2748                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2749                    // TABLE <t>` returns a 2-column row: (Table,
2750                    // Create Table). mysqldump emits this for every
2751                    // table at scrape time; without it the dump
2752                    // round-trip stalls.
2753                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2754                    // FROM <t>` (also spelled `SHOW INDEX` and
2755                    // `SHOW KEYS`). admin / mysqldump probes use
2756                    // it to list per-table indexes.
2757                    "indexes" | "index" | "keys" => {
2758                        if !matches!(self.peek(), Token::From) {
2759                            return Err(self.err(format!(
2760                                "expected FROM after SHOW INDEXES, got {:?}",
2761                                self.peek()
2762                            )));
2763                        }
2764                        self.advance();
2765                        let table = self.expect_ident_like()?;
2766                        Ok(Statement::ShowIndexes(table))
2767                    }
2768                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2769                    // `SHOW VARIABLES`. Both return a 2-column row
2770                    // set listing server-side state; clients probe
2771                    // them at connect time.
2772                    "status" => Ok(Statement::ShowStatus),
2773                    "variables" => {
2774                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2775                        if matches!(self.peek(), Token::Like) {
2776                            self.advance();
2777                            let pat = match self.advance() {
2778                                Token::String(p) => p,
2779                                other => {
2780                                    return Err(self.err(format!(
2781                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2782                                    )));
2783                                }
2784                            };
2785                            return Ok(Statement::ShowVariablesLike(pat));
2786                        }
2787                        Ok(Statement::ShowVariables)
2788                    }
2789                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2790                    "processlist" => Ok(Statement::ShowProcesslist),
2791                    "create" => {
2792                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2793                        // TABLE is supported in v7.17.
2794                        let kind = match self.advance() {
2795                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2796                            Token::Table => "table".to_string(),
2797                            other => {
2798                                return Err(self.err(format!(
2799                                    "expected TABLE after SHOW CREATE, got {other:?}"
2800                                )));
2801                            }
2802                        };
2803                        if !kind.eq_ignore_ascii_case("table") {
2804                            return Err(self.err(format!(
2805                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2806                            )));
2807                        }
2808                        let name = self.expect_ident_like()?;
2809                        Ok(Statement::ShowCreateTable(name))
2810                    }
2811                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2812                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2813                    // it to populate the database selector at connect
2814                    // time; without it `mysql -p` errors before the
2815                    // first user query.
2816                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2817                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2818                    // keyword on its own; it lands here as a bare
2819                    // ident. Returning all publications + their
2820                    // scope summary.
2821                    "publications" => Ok(Statement::ShowPublications),
2822                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2823                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2824                    "columns" => {
2825                        if !matches!(self.peek(), Token::From) {
2826                            return Err(self.err(format!(
2827                                "expected FROM after SHOW COLUMNS, got {:?}",
2828                                self.peek()
2829                            )));
2830                        }
2831                        self.advance();
2832                        let table = self.expect_ident_like()?;
2833                        Ok(Statement::ShowColumns(table))
2834                    }
2835                    // v7.38 轴 4 surface — `SHOW <param>` for any
2836                    // remaining session / preset parameter name
2837                    // (server_version, search_path, client_encoding,
2838                    // …). The engine's ShowParameter handler does the
2839                    // dispatch; unrecognised names error there with
2840                    // a pointer to pg_settings, not at parse time —
2841                    // so a driver that issues `SHOW spam_setting`
2842                    // gets a clear runtime error instead of a
2843                    // confusing "unknown SHOW target".
2844                    other => {
2845                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2846                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2847                        // consume the dotted tail so it round-trips with
2848                        // `SET app.foo` / `current_setting('app.foo')`.
2849                        let mut full = other.to_string();
2850                        while matches!(self.peek(), Token::Dot) {
2851                            self.advance();
2852                            let seg = self.expect_ident_like()?;
2853                            full.push('.');
2854                            full.push_str(&seg.to_ascii_lowercase());
2855                        }
2856                        Ok(Statement::ShowParameter(full))
2857                    }
2858                }
2859            }
2860            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2861            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2862            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2863            // arrived as a bare ident; tokenising it dedicatedly
2864            // keeps the dispatch tree small.
2865            Token::Drop => {
2866                self.advance();
2867                match self.peek() {
2868                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2869                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2870                    // around DROP ROLE cleanup. SPG has no role-owner
2871                    // model, so consume to boundary as a no-op.
2872                    Token::Ident(s) | Token::QuotedIdent(s)
2873                        if s.eq_ignore_ascii_case("owned") =>
2874                    {
2875                        // v7.39 (round 696) — still a no-op (SPG has no
2876                        // role-owner model), but the ROLE is carried out so
2877                        // the engine can refuse one that does not exist,
2878                        // which is what PG18 does.
2879                        self.advance();
2880                        if self.peek_is_by() {
2881                            self.advance();
2882                        }
2883                        let names = self.take_comma_separated_names();
2884                        self.consume_until_statement_boundary();
2885                        Ok(Statement::ValidateOnly {
2886                            kind: crate::ast::ValidateOnlyKind::RoleName,
2887                            names,
2888                        })
2889                    }
2890                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2891                    // It drops only a TEMPORARY table, and name resolution
2892                    // already prefers the session's own, so the keyword is
2893                    // consumed and the ordinary DROP TABLE path runs.
2894                    Token::Ident(s) | Token::QuotedIdent(s)
2895                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2896                    {
2897                        self.advance();
2898                        if !matches!(self.peek(), Token::Table) {
2899                            return Err(self.err(alloc::format!(
2900                                "expected TABLE after DROP TEMPORARY, got {:?}",
2901                                self.peek()
2902                            )));
2903                        }
2904                        self.parse_drop_table_after_keyword()
2905                    }
2906                    Token::Publication => {
2907                        self.advance();
2908                        // v7.39 (round 754, F31-B4) — the round-753
2909                        // audit probe tripped over the missing
2910                        // `IF EXISTS` here (syntax error).
2911                        let if_exists = self.consume_if_exists();
2912                        let name = self.expect_ident_or_string()?;
2913                        Ok(Statement::DropPublication { name, if_exists })
2914                    }
2915                    Token::Subscription => {
2916                        self.advance();
2917                        let if_exists = self.consume_if_exists();
2918                        let name = self.expect_ident_or_string()?;
2919                        Ok(Statement::DropSubscription { name, if_exists })
2920                    }
2921                    Token::Ident(s) | Token::QuotedIdent(s)
2922                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2923                    {
2924                        self.advance();
2925                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2926                        // login user IS a role in PG, and SPG's store holds
2927                        // both. `IF EXISTS` is accepted on either spelling.
2928                        let if_exists = self.consume_if_exists();
2929                        let name = self.expect_ident_or_string()?;
2930                        Ok(Statement::DropUser { name, if_exists })
2931                    }
2932                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2933                    // CREATE DATABASE has parsed since v7.14 and this did
2934                    // not, so `DROP DATABASE IF EXISTS x` — what every
2935                    // teardown script and pg_dumpall preamble opens with —
2936                    // came back as a syntax error, which IF EXISTS cannot
2937                    // soften. The name is carried so the engine can answer
2938                    // the way PG does; PG never lets this succeed on a
2939                    // single-database server, since the name is either
2940                    // unknown ("database … does not exist", or a notice
2941                    // under IF EXISTS) or the one you are connected to
2942                    // ("cannot drop the currently open database").
2943                    Token::Ident(s) | Token::QuotedIdent(s)
2944                        if s.eq_ignore_ascii_case("database") =>
2945                    {
2946                        self.advance();
2947                        let if_exists = self.consume_if_exists();
2948                        let name = self.expect_ident_or_string()?;
2949                        self.consume_until_statement_boundary();
2950                        Ok(Statement::DropDatabase { name, if_exists })
2951                    }
2952                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2953                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2954                        self.advance();
2955                        let if_exists = self.consume_if_exists();
2956                        let name = self.expect_ident_like()?;
2957                        // ON <table>
2958                        if !matches!(self.peek(), Token::On) {
2959                            return Err(self.err(alloc::format!(
2960                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2961                                self.peek()
2962                            )));
2963                        }
2964                        self.advance();
2965                        let table = self.expect_ident_like()?;
2966                        Ok(Statement::DropTrigger {
2967                            name,
2968                            table,
2969                            if_exists,
2970                        })
2971                    }
2972                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2973                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2974                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2975                        self.advance();
2976                        let if_exists = self.consume_if_exists();
2977                        let name = self.expect_ident_like()?;
2978                        if !matches!(self.peek(), Token::On) {
2979                            return Err(self.err(alloc::format!(
2980                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
2981                                self.peek()
2982                            )));
2983                        }
2984                        self.advance();
2985                        let table = self.expect_ident_like()?;
2986                        // Optional CASCADE / RESTRICT — accepted, no effect.
2987                        self.consume_until_statement_boundary();
2988                        Ok(Statement::DropRule {
2989                            name,
2990                            table,
2991                            if_exists,
2992                        })
2993                    }
2994                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2995                    // v7.12.4 ignores any optional arg-list (signature-
2996                    // based overload disambiguation lands in v7.12.5+).
2997                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2998                        self.advance();
2999                        let if_exists = self.consume_if_exists();
3000                        let name = self.expect_ident_like()?;
3001                        // v7.39 (read01 round 62) — the argument list identifies
3002                        // WHICH overload to drop, so it is captured, not
3003                        // discarded. `DROP FUNCTION f` (no list) is legal when
3004                        // the name is unambiguous; the engine enforces that.
3005                        let args = if matches!(self.peek(), Token::LParen) {
3006                            Some(self.parse_function_signature_types()?)
3007                        } else {
3008                            None
3009                        };
3010                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3011                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3012                        // accepted since v7.14 and this one refused outright.
3013                        // pg_dump writes it, so refusing was a parse error in
3014                        // the middle of a restore. SPG drops the function
3015                        // either way — it tracks no dependents to cascade to —
3016                        // which is the same reading the other two give it.
3017                        self.consume_drop_behaviour();
3018                        Ok(Statement::DropFunction {
3019                            name,
3020                            args,
3021                            if_exists,
3022                        })
3023                    }
3024                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3025                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3026                    // emit DROP TABLE IF EXISTS at the head of every
3027                    // CREATE TABLE block so re-importing a dump
3028                    // overwrites prior state. SPG accepts and removes
3029                    // matching tables; CASCADE/RESTRICT trailers
3030                    // accepted silently.
3031                    Token::Table => self.parse_drop_table_after_keyword(),
3032                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3033                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3034                    // for partial-index renames and pgvector
3035                    // migrations. SPG removes the matching index;
3036                    // IF EXISTS makes the drop idempotent.
3037                    Token::Index => {
3038                        self.advance();
3039                        let if_exists = self.consume_if_exists();
3040                        let name = self.expect_ident_like()?;
3041                        if matches!(
3042                            self.peek(),
3043                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3044                                || s.eq_ignore_ascii_case("restrict")
3045                        ) {
3046                            self.advance();
3047                        }
3048                        Ok(Statement::DropIndex { name, if_exists })
3049                    }
3050                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3051                    // [CASCADE|RESTRICT]. SPG is single-database;
3052                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3053                    // name [, name…] [CASCADE | RESTRICT]. Real
3054                    // unregister (was silent no-op pre-v7.17).
3055                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3056                        self.advance();
3057                        let if_exists = self.consume_if_exists();
3058                        let mut names = vec![self.expect_ident_like()?];
3059                        while matches!(self.peek(), Token::Comma) {
3060                            self.advance();
3061                            names.push(self.expect_ident_like()?);
3062                        }
3063                        if matches!(
3064                            self.peek(),
3065                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3066                                || s.eq_ignore_ascii_case("restrict")
3067                        ) {
3068                            self.advance();
3069                        }
3070                        Ok(Statement::DropSchema { names, if_exists })
3071                    }
3072                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3073                    // name [, name…] [CASCADE|RESTRICT].
3074                    Token::Ident(s) | Token::QuotedIdent(s)
3075                        if s.eq_ignore_ascii_case("type") =>
3076                    {
3077                        self.advance();
3078                        let if_exists = self.consume_if_exists();
3079                        let mut names = vec![self.expect_ident_like()?];
3080                        while matches!(self.peek(), Token::Comma) {
3081                            self.advance();
3082                            names.push(self.expect_ident_like()?);
3083                        }
3084                        if matches!(
3085                            self.peek(),
3086                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3087                                || s.eq_ignore_ascii_case("restrict")
3088                        ) {
3089                            self.advance();
3090                        }
3091                        Ok(Statement::DropType { names, if_exists })
3092                    }
3093                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3094                    // name [, name…] [CASCADE|RESTRICT].
3095                    Token::Ident(s) | Token::QuotedIdent(s)
3096                        if s.eq_ignore_ascii_case("domain") =>
3097                    {
3098                        self.advance();
3099                        let if_exists = self.consume_if_exists();
3100                        let mut names = vec![self.expect_ident_like()?];
3101                        while matches!(self.peek(), Token::Comma) {
3102                            self.advance();
3103                            names.push(self.expect_ident_like()?);
3104                        }
3105                        if matches!(
3106                            self.peek(),
3107                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3108                                || s.eq_ignore_ascii_case("restrict")
3109                        ) {
3110                            self.advance();
3111                        }
3112                        Ok(Statement::DropDomain { names, if_exists })
3113                    }
3114                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3115                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3116                    Token::Ident(s) | Token::QuotedIdent(s)
3117                        if s.eq_ignore_ascii_case("materialized") =>
3118                    {
3119                        self.advance();
3120                        let nxt = self.peek().clone();
3121                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3122                        {
3123                            return Err(self.err(alloc::format!(
3124                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3125                            )));
3126                        }
3127                        self.advance();
3128                        let if_exists = self.consume_if_exists();
3129                        let mut names = vec![self.expect_ident_like()?];
3130                        while matches!(self.peek(), Token::Comma) {
3131                            self.advance();
3132                            names.push(self.expect_ident_like()?);
3133                        }
3134                        if matches!(
3135                            self.peek(),
3136                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3137                                || s.eq_ignore_ascii_case("restrict")
3138                        ) {
3139                            self.advance();
3140                        }
3141                        Ok(Statement::DropMaterializedView { names, if_exists })
3142                    }
3143                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3144                    // name [, name…] [CASCADE|RESTRICT].
3145                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3146                        self.advance();
3147                        let if_exists = self.consume_if_exists();
3148                        let mut names = vec![self.expect_ident_like()?];
3149                        while matches!(self.peek(), Token::Comma) {
3150                            self.advance();
3151                            names.push(self.expect_ident_like()?);
3152                        }
3153                        if matches!(
3154                            self.peek(),
3155                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3156                                || s.eq_ignore_ascii_case("restrict")
3157                        ) {
3158                            self.advance();
3159                        }
3160                        Ok(Statement::DropView { names, if_exists })
3161                    }
3162                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3163                    // [CASCADE|RESTRICT]. Real removal from catalog
3164                    // (was a silent no-op pre-v7.17).
3165                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3166                        self.advance();
3167                        let if_exists = self.consume_if_exists();
3168                        let mut names = vec![self.expect_ident_like()?];
3169                        while matches!(self.peek(), Token::Comma) {
3170                            self.advance();
3171                            names.push(self.expect_ident_like()?);
3172                        }
3173                        if matches!(
3174                            self.peek(),
3175                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3176                                || s.eq_ignore_ascii_case("restrict")
3177                        ) {
3178                            self.advance();
3179                        }
3180                        Ok(Statement::DropSequence { names, if_exists })
3181                    }
3182                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3183                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3184                        self.advance();
3185                        self.parse_drop_policy_after_keyword()
3186                    }
3187                    // v7.37.17 (17.6 siblings) — DROP <target> for
3188                    // targets SPG doesn't natively track. pg_dump
3189                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3190                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3191                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3192                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3193                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3194                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3195                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3196                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3197                    // etc. — accept + Empty-return so pg_dump tails
3198                    // load through. Materialized-view drop dispatches
3199                    // to the existing DropTable path when the token
3200                    // is Materialized-View-shaped (elsewhere in
3201                    // this parser).
3202                    Token::Ident(s) | Token::QuotedIdent(s)
3203                        if s.eq_ignore_ascii_case("text")
3204                            // The DROP dispatch matches on PEEK — `text` is
3205                            // not yet consumed, so SEARCH/CONFIGURATION sit
3206                            // at pos+1/pos+2 (the round-695 trap's mirror).
3207                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3208                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3209                    {
3210                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3211                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3212                        // stay in the noise arm below.
3213                        self.advance(); // TEXT
3214                        self.advance(); // SEARCH
3215                        self.advance(); // CONFIGURATION
3216                        let if_exists = self.consume_if_exists();
3217                        let names = self.take_comma_separated_names();
3218                        self.consume_until_statement_boundary();
3219                        if if_exists {
3220                            return Ok(Statement::Empty);
3221                        }
3222                        Ok(Statement::ValidateOnly {
3223                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3224                            names,
3225                        })
3226                    }
3227                    Token::Ident(s) | Token::QuotedIdent(s)
3228                        if matches!(
3229                            s.to_ascii_lowercase().as_str(),
3230                            "type"
3231                                | "domain"
3232                                | "operator"
3233                                | "cast"
3234                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3235                                // TEMPLATE (CONFIGURATION intercepted above).
3236                                | "text"
3237                                | "materialized"
3238                                | "large"
3239                                | "role"
3240                                | "access"
3241                                | "procedure"
3242                                | "routine"
3243                        ) =>
3244                    {
3245                        self.consume_until_statement_boundary();
3246                        Ok(Statement::Empty)
3247                    }
3248                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3249                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3250                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3251                    // foreign-data warning family (round 706) so a
3252                    // CREATE→DROP sequence in a dump stays consistent.
3253                    Token::Ident(s) | Token::QuotedIdent(s)
3254                        if s.eq_ignore_ascii_case("server")
3255                            || s.eq_ignore_ascii_case("foreign") =>
3256                    {
3257                        self.advance();
3258                        self.consume_until_statement_boundary();
3259                        Ok(Statement::ValidateOnly {
3260                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3261                            names: Vec::new(),
3262                        })
3263                    }
3264                    Token::Ident(s) | Token::QuotedIdent(s)
3265                        if s.eq_ignore_ascii_case("collation")
3266                            || s.eq_ignore_ascii_case("tablespace") =>
3267                    {
3268                        let kind = if s.eq_ignore_ascii_case("collation") {
3269                            crate::ast::ValidateOnlyKind::CollationName
3270                        } else {
3271                            crate::ast::ValidateOnlyKind::TablespaceName
3272                        };
3273                        self.advance();
3274                        let if_exists = self.consume_if_exists();
3275                        let names = self.take_comma_separated_names();
3276                        self.consume_until_statement_boundary();
3277                        if if_exists {
3278                            return Ok(Statement::Empty);
3279                        }
3280                        Ok(Statement::ValidateOnly { kind, names })
3281                    }
3282                    Token::Ident(s) | Token::QuotedIdent(s)
3283                        if s.eq_ignore_ascii_case("event") =>
3284                    {
3285                        self.advance();
3286                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3287                        {
3288                            self.advance();
3289                        }
3290                        let if_exists = self.consume_if_exists();
3291                        let names = self.take_comma_separated_names();
3292                        self.consume_until_statement_boundary();
3293                        if if_exists {
3294                            return Ok(Statement::Empty);
3295                        }
3296                        Ok(Statement::ValidateOnly {
3297                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3298                            names,
3299                        })
3300                    }
3301                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3302                    // leave the noise list; see the ValidateOnly kinds.
3303                    Token::Ident(s) | Token::QuotedIdent(s)
3304                        if s.eq_ignore_ascii_case("conversion")
3305                            || s.eq_ignore_ascii_case("language")
3306                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3307                            // FIRST — the first draft looked for it after.
3308                            || s.eq_ignore_ascii_case("procedural") =>
3309                    {
3310                        let kind = if s.eq_ignore_ascii_case("conversion") {
3311                            crate::ast::ValidateOnlyKind::ConversionName
3312                        } else {
3313                            crate::ast::ValidateOnlyKind::LanguageName
3314                        };
3315                        self.advance();
3316                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3317                        {
3318                            self.advance();
3319                        }
3320                        let if_exists = self.consume_if_exists();
3321                        let names = self.take_comma_separated_names();
3322                        self.consume_until_statement_boundary();
3323                        if if_exists {
3324                            return Ok(Statement::Empty);
3325                        }
3326                        Ok(Statement::ValidateOnly { kind, names })
3327                    }
3328                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3329                    // name(argtypes)[, …]`. Parsed for real so the engine
3330                    // can answer as PG does; see Statement::DropAggregate.
3331                    Token::Ident(s) | Token::QuotedIdent(s)
3332                        if s.eq_ignore_ascii_case("aggregate") =>
3333                    {
3334                        self.advance();
3335                        let if_exists = self.consume_if_exists();
3336                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3337                        loop {
3338                            let name = self.expect_ident_like()?;
3339                            if !matches!(self.peek(), Token::LParen) {
3340                                return Err(self.err(alloc::format!(
3341                                    "expected argument list after DROP AGGREGATE {name}"
3342                                )));
3343                            }
3344                            self.advance();
3345                            let mut args: Vec<String> = Vec::new();
3346                            let mut star = false;
3347                            loop {
3348                                match self.peek().clone() {
3349                                    Token::RParen => {
3350                                        self.advance();
3351                                        break;
3352                                    }
3353                                    Token::Star => {
3354                                        self.advance();
3355                                        star = true;
3356                                    }
3357                                    Token::Comma => {
3358                                        self.advance();
3359                                    }
3360                                    _ => {
3361                                        // A type name may be multi-token
3362                                        // (`double precision`); glue idents
3363                                        // until , or ).
3364                                        let mut t = self.expect_ident_like()?;
3365                                        while let Token::Ident(nx) = self.peek() {
3366                                            let nx = nx.clone();
3367                                            self.advance();
3368                                            t.push(' ');
3369                                            t.push_str(&nx);
3370                                        }
3371                                        args.push(t);
3372                                    }
3373                                }
3374                            }
3375                            items.push((name, if star { None } else { Some(args) }));
3376                            if matches!(self.peek(), Token::Comma) {
3377                                self.advance();
3378                            } else {
3379                                break;
3380                            }
3381                        }
3382                        self.consume_until_statement_boundary();
3383                        Ok(Statement::DropAggregate { if_exists, items })
3384                    }
3385                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3386                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3387                    // installed; `IF EXISTS` is the spelling that says do
3388                    // not, and it keeps the no-op.
3389                    Token::Ident(s) | Token::QuotedIdent(s)
3390                        if s.eq_ignore_ascii_case("extension") =>
3391                    {
3392                        self.advance();
3393                        let if_exists = self.consume_if_exists();
3394                        let names = self.take_comma_separated_names();
3395                        self.consume_until_statement_boundary();
3396                        if if_exists {
3397                            return Ok(Statement::Empty);
3398                        }
3399                        Ok(Statement::ValidateOnly {
3400                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3401                            names,
3402                        })
3403                    }
3404                    Token::Ident(s) | Token::QuotedIdent(s)
3405                        if s.eq_ignore_ascii_case("statistics") =>
3406                    {
3407                        self.parse_drop_statistics_after_drop()
3408                    }
3409                    other => Err(self.err(format!(
3410                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3411                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3412                    ))),
3413                }
3414            }
3415            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3416            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3417            // and accepted before the view name. SPG materialised
3418            // views re-evaluate on read (always-fresh semantics), so
3419            // the CONCURRENTLY-vs-serial distinction has no runtime
3420            // effect — the refresh body does not block readers either
3421            // way. Same accept-and-no-op pattern as DETACH PARTITION
3422            // CONCURRENTLY (16.5).
3423            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3424                self.advance();
3425                let nxt = self.peek().clone();
3426                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3427                {
3428                    return Err(self.err(alloc::format!(
3429                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3430                    )));
3431                }
3432                self.advance();
3433                let nxt2 = self.peek().clone();
3434                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3435                {
3436                    return Err(self.err(alloc::format!(
3437                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3438                    )));
3439                }
3440                self.advance();
3441                // Optional CONCURRENTLY noise word — consumed without
3442                // changing semantics.
3443                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3444                {
3445                    self.advance();
3446                }
3447                let name = self.expect_ident_like()?;
3448                let with_data = self.parse_optional_with_data(true)?;
3449                Ok(Statement::RefreshMaterializedView { name, with_data })
3450            }
3451            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3452                self.advance();
3453                self.parse_update_after_keyword()
3454            }
3455            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3456            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3457            // [CASCADE | RESTRICT]. Clears every row from each named
3458            // table. Parses at the top level; the engine dispatcher
3459            // walks Statement::Truncate.
3460            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3461                self.advance();
3462                // Optional TABLE noise word — PG accepts both the reserved
3463                // token and the bare identifier spelling.
3464                if matches!(self.peek(), Token::Table)
3465                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3466                {
3467                    self.advance();
3468                }
3469                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3470                // not absorbed. The lookahead keeps a table genuinely
3471                // called `only` working: the keyword is a keyword only
3472                // when a name follows it.
3473                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3474                    if s.eq_ignore_ascii_case("only"))
3475                    && matches!(
3476                        self.tokens.get(self.pos + 1),
3477                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3478                    );
3479                if only {
3480                    self.advance();
3481                }
3482                // Table names (comma-separated).
3483                let mut tables = Vec::new();
3484                loop {
3485                    tables.push(self.expect_ident_like()?);
3486                    if matches!(self.peek(), Token::Comma) {
3487                        self.advance();
3488                        continue;
3489                    }
3490                    break;
3491                }
3492                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3493                let mut restart_identity = false;
3494                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3495                {
3496                    self.advance();
3497                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3498                    {
3499                        self.advance();
3500                        restart_identity = true;
3501                    }
3502                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3503                {
3504                    self.advance();
3505                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3506                    {
3507                        self.advance();
3508                    }
3509                }
3510                // Optional CASCADE / RESTRICT.
3511                let mut cascade = false;
3512                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3513                {
3514                    self.advance();
3515                    cascade = true;
3516                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3517                {
3518                    self.advance();
3519                }
3520                Ok(Statement::Truncate {
3521                    tables,
3522                    restart_identity,
3523                    cascade,
3524                    only,
3525                })
3526            }
3527            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3528            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3529            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3530            // rows change so the index tree is always up-to-date;
3531            // REINDEX is a strict no-op. Accept the whole statement
3532            // shape to boundary for pg_dump round-trip compatibility.
3533            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3534                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3535                // index bloat to rebuild, so the work stays a no-op, but PG
3536                // validates what it was pointed at and this swallowed the
3537                // name at parse time — `REINDEX TABLE typo` reported
3538                // success. Measured on PG18: INDEX / TABLE name a relation,
3539                // SCHEMA a schema, SYSTEM nothing.
3540                self.advance();
3541                self.parse_reindex_tail()
3542            }
3543            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3544            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3545            // SPG has no MVCC bloat today (Phase D visibility map
3546            // queues with v7.38); the freezer collapses hot-tier
3547            // rows into cold segments automatically. VACUUM is a
3548            // no-op — pg_dump maintenance scripts and Discourse's
3549            // periodic-maintenance path both emit it.
3550            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3551            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3552            // actual bloat, so the pre-MVCC accept-and-ignore posture
3553            // became a silent no-op on a customer's manual reclaim.
3554            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3555            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3556            // ANALYZE is captured, the optional table name is captured.
3557            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3558                self.advance();
3559                // Parenthesised option list: absorb it.
3560                if matches!(self.peek(), Token::LParen) {
3561                    let mut depth = 0usize;
3562                    loop {
3563                        match self.advance() {
3564                            Token::LParen => depth += 1,
3565                            Token::RParen => {
3566                                depth -= 1;
3567                                if depth == 0 {
3568                                    break;
3569                                }
3570                            }
3571                            Token::Eof => break,
3572                            _ => {}
3573                        }
3574                    }
3575                }
3576                let mut analyze = false;
3577                let mut table: Option<String> = None;
3578                loop {
3579                    match self.peek() {
3580                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3581                        // an identifier, so the loop below broke out on it and
3582                        // dropped the table name: `VACUUM FULL nosuch` was
3583                        // accepted where `VACUUM nosuch` was refused.
3584                        Token::Full => {
3585                            self.advance();
3586                        }
3587                        Token::Ident(w) | Token::QuotedIdent(w) => {
3588                            let wl = w.to_ascii_lowercase();
3589                            match wl.as_str() {
3590                                "full" | "freeze" | "verbose" => {
3591                                    self.advance();
3592                                }
3593                                "analyze" | "analyse" => {
3594                                    analyze = true;
3595                                    self.advance();
3596                                }
3597                                _ => {
3598                                    table = Some(self.expect_ident_like()?);
3599                                    break;
3600                                }
3601                            }
3602                        }
3603                        _ => break,
3604                    }
3605                }
3606                // Optional trailing column list / anything else to the
3607                // statement boundary (PG accepts per-column ANALYZE).
3608                self.consume_until_statement_boundary();
3609                Ok(Statement::Vacuum { table, analyze })
3610            }
3611            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3612            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3613            // <index>. PG stores rows in physical order matching
3614            // an index; SPG's hot-tier is append-only + cold-tier
3615            // is segment-frozen, so clustering has no persistent
3616            // effect. Accept-and-no-op for pg_dump compat.
3617            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3618                // v7.39 (round 535) — same as REINDEX above: the relation is
3619                // carried so the engine can refuse one that does not exist.
3620                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3621                self.advance();
3622                self.parse_cluster_tail()
3623            }
3624            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3625            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3626            // optional string payload; UNLISTEN takes a channel or `*`.
3627            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3628                self.advance();
3629                let ch = match self.advance() {
3630                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3631                    other => {
3632                        return Err(self.err(format!(
3633                            "expected channel name after LISTEN, got {other:?}"
3634                        )));
3635                    }
3636                };
3637                Ok(Statement::Listen(ch))
3638            }
3639            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3640                self.advance();
3641                let channel = match self.advance() {
3642                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3643                    other => {
3644                        return Err(self.err(format!(
3645                            "expected channel name after NOTIFY, got {other:?}"
3646                        )));
3647                    }
3648                };
3649                let payload = if matches!(self.peek(), Token::Comma) {
3650                    self.advance();
3651                    match self.advance() {
3652                        Token::String(p) => Some(p),
3653                        other => {
3654                            return Err(self.err(format!(
3655                                "expected string payload after NOTIFY <channel>, got {other:?}"
3656                            )));
3657                        }
3658                    }
3659                } else {
3660                    None
3661                };
3662                Ok(Statement::Notify { channel, payload })
3663            }
3664            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3665                self.advance();
3666                match self.advance() {
3667                    Token::Star => Ok(Statement::Unlisten(None)),
3668                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3669                    other => Err(self.err(format!(
3670                        "expected channel name or * after UNLISTEN, got {other:?}"
3671                    ))),
3672                }
3673            }
3674            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3675            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3676            // process-wide write lock today; explicit LOCK has no
3677            // effect. Accept-and-no-op for pg_dump / migration
3678            // compat.
3679            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3680                self.advance();
3681                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3682                // engine holds a process-wide write lock), but the TABLE
3683                // NAME is now carried out so the engine can refuse one that
3684                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3685                // READ|WRITE` is a different statement with the same first
3686                // word; it keeps the old no-op, because a MySQL dump's
3687                // bracket names tables it is about to create.
3688                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3689                    if k.eq_ignore_ascii_case("tables"));
3690                if mysql_tables {
3691                    self.consume_until_statement_boundary();
3692                    return Ok(Statement::Empty);
3693                }
3694                if matches!(self.peek(), Token::Table) {
3695                    self.advance();
3696                }
3697                let names = self.take_comma_separated_names();
3698                self.consume_until_statement_boundary();
3699                Ok(Statement::ValidateOnly {
3700                    kind: crate::ast::ValidateOnlyKind::LockTable,
3701                    names,
3702                })
3703            }
3704            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3705            // durability marker + snapshot in PG. SPG has WAL
3706            // checkpointing on a byte / time schedule (v7.37.10
3707            // 60s / 4 MiB defaults). The bare statement parses to
3708            // `Statement::Empty` here (the no_std engine owns no
3709            // WAL / snapshot); v7.37 Epic Du wires the HOST
3710            // (embedded `Database::execute_buffered`, via
3711            // `sql_is_checkpoint`) to force an immediate synchronous
3712            // checkpoint through `Database::checkpoint` — a real
3713            // durability barrier, matching PG.
3714            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3715                self.advance();
3716                self.consume_until_statement_boundary();
3717                Ok(Statement::Empty)
3718            }
3719            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3720                self.advance();
3721                self.parse_delete_after_keyword()
3722            }
3723            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3724            // ALTER is not a reserved keyword in the lexer — handled
3725            // as a bare ident here.
3726            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3727                self.advance();
3728                self.parse_alter_after_keyword()
3729            }
3730            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3731            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3732            // additions needed.
3733            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3734                self.advance();
3735                self.parse_wait_after_keyword()
3736            }
3737            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3738            // Bare ANALYZE → analyse every user table; ANALYZE
3739            // <name> → re-stats one. The argument is an optional
3740            // ident (or quoted ident); anything else is a parse
3741            // error.
3742            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3743            // `WHERE` filter (carved out per V6_7_DESIGN.md
3744            // STABILITY). Lex order: identifier "compact" → "cold"
3745            // → "segments". Anything else after `COMPACT` is a
3746            // parse error.
3747            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3748                self.advance();
3749                let next = self.peek().clone();
3750                let cold = match next {
3751                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3752                    _ => {
3753                        return Err(
3754                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3755                        );
3756                    }
3757                };
3758                if !cold.eq_ignore_ascii_case("cold") {
3759                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3760                }
3761                self.advance();
3762                let next = self.peek().clone();
3763                let segments = match next {
3764                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3765                    _ => {
3766                        return Err(self.err(format!(
3767                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3768                            self.peek()
3769                        )));
3770                    }
3771                };
3772                if !segments.eq_ignore_ascii_case("segments") {
3773                    return Err(self.err(format!(
3774                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3775                    )));
3776                }
3777                self.advance();
3778                Ok(Statement::CompactColdSegments)
3779            }
3780            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3781            // Parsed as a case-insensitive identifier since MERGE
3782            // isn't a reserved lexer keyword (collides with the
3783            // mysqldump `ALGORITHM = MERGE` view clause if it
3784            // were); the inner parser drives the rest of the
3785            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3786            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3787                self.advance();
3788                self.parse_merge_after_keyword()
3789            }
3790            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3791                self.advance();
3792                let target = match self.peek() {
3793                    Token::Eof | Token::Semicolon => None,
3794                    Token::Ident(_) | Token::QuotedIdent(_) => {
3795                        Some(self.expect_ident_like()?)
3796                    }
3797                    other => {
3798                        return Err(self.err(format!(
3799                            "expected table name or end of statement after ANALYZE, got {other:?}"
3800                        )));
3801                    }
3802                };
3803                // v7.39 (round 776, F31 J7) — the per-column form
3804                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3805                // here while the VACUUM arm already consumed it; SPG
3806                // analyzes whole tables, so the list parses and is
3807                // accepted like the VACUUM path's.
3808                if target.is_some() && matches!(self.peek(), Token::LParen) {
3809                    self.advance();
3810                    loop {
3811                        let _ = self.expect_ident_like()?;
3812                        match self.peek() {
3813                            Token::Comma => {
3814                                self.advance();
3815                            }
3816                            Token::RParen => {
3817                                self.advance();
3818                                break;
3819                            }
3820                            other => {
3821                                return Err(self.err(format!(
3822                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3823                                )));
3824                            }
3825                        }
3826                    }
3827                }
3828                Ok(Statement::Analyze(target))
3829            }
3830            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3831            // `default_text_search_config` parameter is consumed
3832            // by the FTS function dispatcher; other parameter
3833            // names are recorded but treated as a no-op so PG
3834            // dump output loads.
3835            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3836                self.advance();
3837                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3838                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3839                // …` which the SessionVar path handles). `LOCAL` is the only
3840                // one that changes semantics — it scopes the change to the
3841                // current transaction — so capture it; SESSION / GLOBAL are
3842                // accepted and treated as the default session scope.
3843                let mut set_local = false;
3844                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3845                    let q = s.to_ascii_lowercase();
3846                    if q == "local" || q == "session" || q == "global" {
3847                        set_local = q == "local";
3848                        self.advance();
3849                    }
3850                }
3851                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3852                // <collation>]` — change the connection client
3853                // charset. SPG stores UTF-8 always and orders
3854                // bytewise; accept as a no-op.
3855                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3856                {
3857                    self.advance();
3858                    // Charset ident-or-string.
3859                    if matches!(
3860                        self.peek(),
3861                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3862                    ) {
3863                        self.advance();
3864                    }
3865                    // Optional `COLLATE <name>`.
3866                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3867                    {
3868                        self.advance();
3869                        if matches!(
3870                            self.peek(),
3871                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3872                        ) {
3873                            self.advance();
3874                        }
3875                    }
3876                    return Ok(Statement::Empty);
3877                }
3878                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3879                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3880                // uses this to switch to the object owner before
3881                // recreating tables. SPG has no role system so this
3882                // is a no-op.
3883                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3884                {
3885                    self.advance(); // ROLE
3886                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3887                    // reset to the login identity; a name / string sets the
3888                    // effective role that drives current_user + RLS.
3889                    let role = match self.peek().clone() {
3890                        Token::Default => {
3891                            self.advance();
3892                            None
3893                        }
3894                        Token::Ident(s) | Token::QuotedIdent(s)
3895                            if s.eq_ignore_ascii_case("none") =>
3896                        {
3897                            self.advance();
3898                            None
3899                        }
3900                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3901                            self.advance();
3902                            Some(s)
3903                        }
3904                        _ => None,
3905                    };
3906                    return Ok(Statement::SetRole(role));
3907                }
3908                // v7.37.17 (17.6 sibling) — PG `SET SESSION
3909                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3910                // ISO SQL surface). pg_dump prepends this to fix
3911                // the isolation level for the restore session. SPG
3912                // defaults to READ COMMITTED and doesn't yet honor
3913                // session-set isolation across statements — accept
3914                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3915                // per-tx form is handled elsewhere.
3916                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3917                {
3918                    self.advance(); // CHARACTERISTICS
3919                    self.consume_until_statement_boundary();
3920                    return Ok(Statement::Empty);
3921                }
3922                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3923                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3924                // pg_dump emits this to control the deferrability of
3925                // FK / UNIQUE constraints across a bulk restore. SPG
3926                // has no deferrable-constraint machinery today; the
3927                // FK checker is strict-immediate. Accept-and-no-op
3928                // for pg_dump round-trip compatibility.
3929                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3930                {
3931                    self.advance(); // CONSTRAINTS
3932                    // v7.39 (round 288) — no longer a no-op: the trailing
3933                    // DEFERRED / IMMEDIATE sets the transaction's timing.
3934                    // v7.39 (round 308, V29) — and the names are kept.
3935                    // They used to be skipped over on the way to the
3936                    // DEFERRED keyword, so a named form silently behaved
3937                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3938                    // every deferrable constraint in the transaction.
3939                    let mut names: alloc::vec::Vec<alloc::string::String> =
3940                        alloc::vec::Vec::new();
3941                    if matches!(self.peek(), Token::All) {
3942                        self.advance();
3943                    } else {
3944                        loop {
3945                            let mut n = self.expect_ident_like()?;
3946                            // A schema-qualified name (`public.fk_a`)
3947                            // identifies the same constraint; PG resolves
3948                            // it by the trailing segment.
3949                            while matches!(self.peek(), Token::Dot) {
3950                                self.advance();
3951                                n = self.expect_ident_like()?;
3952                            }
3953                            names.push(n);
3954                            if matches!(self.peek(), Token::Comma) {
3955                                self.advance();
3956                            } else {
3957                                break;
3958                            }
3959                        }
3960                    }
3961                    let deferred = match self.peek() {
3962                        Token::Ident(s) | Token::QuotedIdent(s)
3963                            if s.eq_ignore_ascii_case("deferred") =>
3964                        {
3965                            true
3966                        }
3967                        Token::Ident(s) | Token::QuotedIdent(s)
3968                            if s.eq_ignore_ascii_case("immediate") =>
3969                        {
3970                            false
3971                        }
3972                        other => {
3973                            return Err(self.err(alloc::format!(
3974                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3975                            )));
3976                        }
3977                    };
3978                    self.advance();
3979                    return Ok(Statement::SetConstraints { names, deferred });
3980                }
3981                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3982                // { DEFAULT | '<role>' | <ident> }` (mailrs
3983                // round-10 A.1). pg_dump preamble emits the
3984                // `DEFAULT` form to reset session authorization.
3985                //
3986                // v7.39 (round 697) — this said "SPG has no role system so
3987                // this is a strict no-op". SPG has had one since round 58;
3988                // the comment outlived it, and with it the reason a name
3989                // that is not a role was accepted here. It still switches
3990                // no authorization — what it does now is refuse a role
3991                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3992                // AUTHORIZATION` (handled by the RESET parser
3993                // elsewhere). Reference:
3994                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3995                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3996                {
3997                    self.advance(); // AUTHORIZATION
3998                    match self.peek().clone() {
3999                        Token::Default => {
4000                            self.advance();
4001                        }
4002                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4003                            self.advance();
4004                            return Ok(Statement::ValidateOnly {
4005                                kind: crate::ast::ValidateOnlyKind::RoleName,
4006                                names: alloc::vec![r],
4007                            });
4008                        }
4009                        other => {
4010                            return Err(self.err(alloc::format!(
4011                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4012                            )));
4013                        }
4014                    }
4015                    return Ok(Statement::Empty);
4016                }
4017                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4018                // ISOLATION LEVEL { READ COMMITTED | READ
4019                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4020                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4021                // PG-standard surface. v7.37.8 accepts the syntax
4022                // and tracks the selected level on
4023                // `Engine::current_isolation_level()`; the actual
4024                // MVCC / SSI semantics implementation lands in
4025                // the 轴 4 isolation framework (separate train).
4026                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4027                // internally; SPG behaves the same (effectively
4028                // READ COMMITTED at every level today).
4029                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4030                {
4031                    self.advance(); // TRANSACTION
4032                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4033                    return Ok(Statement::SetTransaction { isolation: level });
4034                }
4035                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4036                // alias — same accept-as-no-op as SET NAMES.
4037                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4038                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4039                {
4040                    self.advance(); // CHARACTER
4041                    self.advance(); // SET
4042                    if matches!(
4043                        self.peek(),
4044                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4045                    ) {
4046                        self.advance();
4047                    }
4048                    return Ok(Statement::Empty);
4049                }
4050                // v7.39 (GUC) — PG spells the timezone GUC as two
4051                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4052                // where <value> is a string/ident or the LOCAL /
4053                // DEFAULT keyword (both mean "back to the default").
4054                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4055                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4056                {
4057                    self.advance(); // TIME
4058                    self.advance(); // ZONE
4059                    let value = match self.peek().clone() {
4060                        Token::Ident(s)
4061                            if s.eq_ignore_ascii_case("local")
4062                                || s.eq_ignore_ascii_case("default") =>
4063                        {
4064                            self.advance();
4065                            crate::ast::SetValue::Default
4066                        }
4067                        Token::Default => {
4068                            self.advance();
4069                            crate::ast::SetValue::Default
4070                        }
4071                        _ => self.parse_set_value()?,
4072                    };
4073                    return Ok(Statement::SetParameter {
4074                        name: "timezone".into(),
4075                        value,
4076                        local: set_local,
4077                    });
4078                }
4079                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4080                // MySQL USER-variable assignment: its own per-session
4081                // namespace, an arbitrary expression on the right, and `:=`
4082                // as a second spelling of `=`. It used to fall into the
4083                // session-PARAMETER list below, whose values are literals and
4084                // whose store nothing reads back under a `@` name — so the
4085                // assignment reported success and vanished.
4086                //
4087                // A `@@`-prefixed LHS is a real engine setting and keeps the
4088                // old path.
4089                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4090                    return self.parse_set_user_vars();
4091                }
4092                // v7.14.0 — multi-assignment form
4093                // `SET a = 1, b = 2, …`. Single-assignment is the
4094                // 1-element case. Each LHS may be a regular ident
4095                // or a SessionVar (`@VAR` / `@@VAR`).
4096                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4097                loop {
4098                    let lhs = match self.peek().clone() {
4099                        Token::SessionVar(s) => {
4100                            self.advance();
4101                            s
4102                        }
4103                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4104                        other => {
4105                            return Err(self.err(format!(
4106                                "expected parameter name after SET, got {other:?}"
4107                            )));
4108                        }
4109                    };
4110                    // Accept either `=` or the bare `TO` keyword.
4111                    match self.peek() {
4112                        Token::Eq => {
4113                            self.advance();
4114                        }
4115                        Token::To => {
4116                            self.advance();
4117                        }
4118                        other => {
4119                            return Err(self.err(format!(
4120                                "expected `=` or TO after SET {lhs}, got {other:?}"
4121                            )));
4122                        }
4123                    }
4124                    let mut value = self.parse_set_value()?;
4125                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4126                    // `, name TO` continues a MySQL-style multi-assign,
4127                    // anything else is a PG list VALUE
4128                    // (`SET search_path = myschema, public`) folded into
4129                    // one comma-joined string.
4130                    while matches!(self.peek(), Token::Comma) {
4131                        let is_assign = matches!(
4132                            self.tokens.get(self.pos + 1),
4133                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4134                        ) && matches!(
4135                            self.tokens.get(self.pos + 2),
4136                            Some(Token::Eq | Token::To)
4137                        );
4138                        if is_assign {
4139                            break;
4140                        }
4141                        self.advance(); // comma
4142                        let next = self.parse_set_value()?;
4143                        let joined = alloc::format!(
4144                            "{}, {}",
4145                            set_value_text(&value),
4146                            set_value_text(&next)
4147                        );
4148                        value = crate::ast::SetValue::String(joined);
4149                    }
4150                    pairs.push((lhs, value));
4151                    if matches!(self.peek(), Token::Comma) {
4152                        self.advance();
4153                        continue;
4154                    }
4155                    break;
4156                }
4157                if pairs.len() == 1 {
4158                    let (name, value) = pairs.into_iter().next().unwrap();
4159                    Ok(Statement::SetParameter {
4160                        name,
4161                        value,
4162                        local: set_local,
4163                    })
4164                } else {
4165                    Ok(Statement::SetParameterList(pairs))
4166                }
4167            }
4168            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4169            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4170                self.advance();
4171                match self.peek().clone() {
4172                    Token::All => {
4173                        self.advance();
4174                        Ok(Statement::ResetParameter(None))
4175                    }
4176                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4177                        self.advance();
4178                        Ok(Statement::ResetParameter(None))
4179                    }
4180                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4181                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4182                        self.advance();
4183                        Ok(Statement::SetRole(None))
4184                    }
4185                    _ => {
4186                        let name = self.parse_set_param_name()?;
4187                        Ok(Statement::ResetParameter(Some(name)))
4188                    }
4189                }
4190            }
4191            // v7.39 (round 218) — server-side cursors.
4192            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4193            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4194            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4195            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4196                self.advance();
4197                match self.peek().clone() {
4198                    Token::All => {
4199                        self.advance();
4200                        Ok(Statement::CloseCursor { name: None })
4201                    }
4202                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4203                        self.advance();
4204                        Ok(Statement::CloseCursor { name: None })
4205                    }
4206                    Token::Ident(n) | Token::QuotedIdent(n) => {
4207                        self.advance();
4208                        Ok(Statement::CloseCursor { name: Some(n) })
4209                    }
4210                    other => Err(self.err(format!(
4211                        "expected cursor name or ALL after CLOSE, got {other:?}"
4212                    ))),
4213                }
4214            }
4215            other => Err(self.err(format!(
4216                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4217                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4218            ))),
4219        }
4220    }
4221
4222    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4223    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4224    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4225    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4226    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4227        self.advance(); // DECLARE
4228        let name = match self.advance() {
4229            Token::Ident(n) | Token::QuotedIdent(n) => n,
4230            other => {
4231                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4232            }
4233        };
4234        let mut scroll: Option<bool> = None;
4235        loop {
4236            match self.peek() {
4237                Token::Ident(s)
4238                    if s.eq_ignore_ascii_case("binary")
4239                        || s.eq_ignore_ascii_case("insensitive")
4240                        || s.eq_ignore_ascii_case("asensitive") =>
4241                {
4242                    self.advance();
4243                }
4244                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4245                    self.advance();
4246                    scroll = Some(true);
4247                }
4248                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4249                {
4250                    self.advance(); // NO
4251                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4252                        return Err(self.err(format!(
4253                            "expected SCROLL after NO in DECLARE, got {:?}",
4254                            self.peek()
4255                        )));
4256                    }
4257                    self.advance();
4258                    scroll = Some(false);
4259                }
4260                _ => break,
4261            }
4262        }
4263        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4264            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4265        }
4266        self.advance();
4267        let mut hold = false;
4268        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4269            self.advance();
4270            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4271                return Err(self.err(format!(
4272                    "expected HOLD after WITH in DECLARE, got {:?}",
4273                    self.peek()
4274                )));
4275            }
4276            self.advance();
4277            hold = true;
4278        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4279            self.advance();
4280            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4281                return Err(self.err(format!(
4282                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4283                    self.peek()
4284                )));
4285            }
4286            self.advance();
4287        }
4288        if !matches!(self.peek(), Token::For) {
4289            return Err(self.err(format!(
4290                "expected FOR before the cursor query, got {:?}",
4291                self.peek()
4292            )));
4293        }
4294        self.advance();
4295        let query = self.parse_one_statement()?;
4296        Ok(Statement::DeclareCursor {
4297            name,
4298            scroll,
4299            hold,
4300            query: alloc::boxed::Box::new(query),
4301        })
4302    }
4303
4304    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4305    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4306    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4307    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4308        use crate::ast::CursorDirection as D;
4309        self.advance(); // FETCH / MOVE
4310        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4311            let neg = if matches!(this.peek(), Token::Minus) {
4312                this.advance();
4313                true
4314            } else {
4315                false
4316            };
4317            match this.advance() {
4318                Token::Integer(v) => Ok(if neg { -v } else { v }),
4319                other => Err(this.err(format!("expected count, got {other:?}"))),
4320            }
4321        };
4322        let direction = match self.peek().clone() {
4323            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4324                self.advance();
4325                D::Next
4326            }
4327            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4328                self.advance();
4329                D::Prior
4330            }
4331            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4332                self.advance();
4333                D::First
4334            }
4335            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4336                self.advance();
4337                D::Last
4338            }
4339            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4340                self.advance();
4341                D::Absolute(signed_count(self)?)
4342            }
4343            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4344                self.advance();
4345                D::Relative(signed_count(self)?)
4346            }
4347            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4348                self.advance();
4349                match self.peek().clone() {
4350                    Token::All => {
4351                        self.advance();
4352                        D::All
4353                    }
4354                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4355                        self.advance();
4356                        D::All
4357                    }
4358                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4359                    _ => D::Next, // bare FORWARD = FORWARD 1
4360                }
4361            }
4362            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4363                self.advance();
4364                match self.peek().clone() {
4365                    Token::All => {
4366                        self.advance();
4367                        D::BackwardAll
4368                    }
4369                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4370                        self.advance();
4371                        D::BackwardAll
4372                    }
4373                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4374                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4375                }
4376            }
4377            Token::All => {
4378                self.advance();
4379                D::All
4380            }
4381            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4382                self.advance();
4383                D::All
4384            }
4385            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4386            // Bare `FETCH <name>` — direction defaults to NEXT.
4387            _ => D::Next,
4388        };
4389        // Optional FROM / IN.
4390        if matches!(self.peek(), Token::From)
4391            || matches!(self.peek(), Token::In)
4392            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4393        {
4394            self.advance();
4395        }
4396        let name = match self.advance() {
4397            Token::Ident(n) | Token::QuotedIdent(n) => n,
4398            other => {
4399                return Err(self.err(format!("expected cursor name, got {other:?}")));
4400            }
4401        };
4402        Ok(if is_move {
4403            Statement::MoveCursor { name, direction }
4404        } else {
4405            Statement::FetchCursor { name, direction }
4406        })
4407    }
4408
4409    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4410    /// [(kind, …)] ON <col>, … FROM <table>`.
4411    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4412        self.advance(); // STATISTICS
4413        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4414        let mut if_not_exists = false;
4415        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4416            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4417        {
4418            self.advance();
4419            self.advance();
4420            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4421                self.advance();
4422                if_not_exists = true;
4423            }
4424        }
4425        let name = self.expect_ident_like()?;
4426        let mut kinds = Vec::new();
4427        if matches!(self.peek(), Token::LParen) {
4428            self.advance();
4429            loop {
4430                let k = self.expect_ident_like()?;
4431                // PG stores the single letters; accept the spelled-out
4432                // names the SQL uses and record what PG records.
4433                kinds.push(match k.to_ascii_lowercase().as_str() {
4434                    "ndistinct" => String::from("d"),
4435                    "dependencies" => String::from("f"),
4436                    "mcv" => String::from("m"),
4437                    other => {
4438                        return Err(
4439                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4440                        );
4441                    }
4442                });
4443                match self.advance() {
4444                    Token::Comma => {}
4445                    Token::RParen => break,
4446                    other => {
4447                        return Err(self.err(alloc::format!(
4448                            "expected ',' or ')' in statistics kind list, got {other:?}"
4449                        )));
4450                    }
4451                }
4452            }
4453        }
4454        if !matches!(self.peek(), Token::On) {
4455            return Err(self.err(alloc::format!(
4456                "expected ON in CREATE STATISTICS, got {:?}",
4457                self.peek()
4458            )));
4459        }
4460        self.advance();
4461        let mut columns = Vec::new();
4462        loop {
4463            columns.push(self.expect_ident_like()?);
4464            if matches!(self.peek(), Token::Comma) {
4465                self.advance();
4466            } else {
4467                break;
4468            }
4469        }
4470        if !matches!(self.peek(), Token::From) {
4471            return Err(self.err(alloc::format!(
4472                "expected FROM in CREATE STATISTICS, got {:?}",
4473                self.peek()
4474            )));
4475        }
4476        self.advance();
4477        let table = self.expect_ident_like()?;
4478        Ok(Statement::CreateStatistics {
4479            name,
4480            if_not_exists,
4481            kinds,
4482            columns,
4483            table,
4484        })
4485    }
4486
4487    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4488    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4489    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4490    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4491    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4492    /// forward call.
4493    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4494        self.advance(); // TABLE
4495        let if_exists = self.consume_if_exists();
4496        let mut names: Vec<String> = Vec::new();
4497        loop {
4498            names.push(self.expect_ident_like()?);
4499            if matches!(self.peek(), Token::Comma) {
4500                self.advance();
4501                continue;
4502            }
4503            break;
4504        }
4505        if matches!(
4506            self.peek(),
4507            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4508                || s.eq_ignore_ascii_case("restrict")
4509        ) {
4510            self.advance();
4511        }
4512        Ok(Statement::DropTable { names, if_exists })
4513    }
4514
4515    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4516        self.advance(); // STATISTICS
4517        let mut if_exists = false;
4518        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4519            && matches!(self.tokens.get(self.pos + 1),
4520                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4521        {
4522            self.advance();
4523            self.advance();
4524            if_exists = true;
4525        }
4526        let name = self.expect_ident_like()?;
4527        Ok(Statement::DropStatistics { name, if_exists })
4528    }
4529
4530    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4531        debug_assert!(matches!(self.peek(), Token::Create));
4532        self.advance();
4533        match self.peek() {
4534            Token::Table => self.parse_create_table_stmt_after_create(),
4535            Token::Index => self.parse_create_index_stmt_after_create(false),
4536            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4537            // object now. It used to be consumed by the CREATE-noise
4538            // arm, so a pg_dump that declares extended statistics
4539            // restored silently without them and reflection showed
4540            // nothing.
4541            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4542                self.parse_create_statistics_after_create()
4543            }
4544            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4545            // The `UNIQUE` modifier turns a partial index into a
4546            // partial-uniqueness invariant (only rows matching the
4547            // WHERE predicate are checked for duplicates). mailrs
4548            // K1 (3 hits: email_templates default, calendar_events
4549            // master, calendar_events instance).
4550            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4551                self.advance();
4552                if !matches!(self.peek(), Token::Index) {
4553                    return Err(self.err(alloc::format!(
4554                        "expected INDEX after CREATE UNIQUE, got {:?}",
4555                        self.peek()
4556                    )));
4557                }
4558                self.parse_create_index_stmt_after_create(true)
4559            }
4560            Token::Publication => {
4561                self.advance();
4562                self.parse_create_publication_after_keyword()
4563            }
4564            Token::Subscription => {
4565                self.advance();
4566                self.parse_create_subscription_after_keyword()
4567            }
4568            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4569            // USER isn't a reserved keyword — we look for the bare
4570            // identifier so the lexer doesn't have to grow a token.
4571            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4572                self.advance();
4573                self.parse_create_user_after_keyword(true)
4574            }
4575            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4576            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4577            // the default of the LOGIN attribute.
4578            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4579                self.advance();
4580                self.parse_create_user_after_keyword(false)
4581            }
4582            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4583            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4584                self.advance();
4585                self.parse_create_policy_after_keyword()
4586            }
4587            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4588            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4589            // no-op. mailrs follow-up F3.
4590            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4591                self.advance();
4592                self.parse_create_extension_after_keyword()
4593            }
4594            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4595            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4596            // optional; absorb it here and forward to the
4597            // per-kind parsers with the flag. OR is a reserved
4598            // keyword token.
4599            Token::Or => {
4600                self.advance();
4601                let next = self.peek();
4602                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4603                    return Err(self.err(alloc::format!(
4604                        "expected REPLACE after CREATE OR, got {next:?}"
4605                    )));
4606                };
4607                if !s2.eq_ignore_ascii_case("replace") {
4608                    return Err(self.err(alloc::format!(
4609                        "expected REPLACE after CREATE OR, got {s2:?}"
4610                    )));
4611                }
4612                self.advance();
4613                self.parse_create_function_or_trigger_after_or_replace(true)
4614            }
4615            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4616                self.advance();
4617                self.parse_create_function_after_keyword(false)
4618            }
4619            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4620                self.advance();
4621                self.parse_create_trigger_after_keyword(false)
4622            }
4623            // v7.39 (round 139) — CREATE RULE …
4624            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4625                self.advance();
4626                self.parse_create_rule_after_keyword(false)
4627            }
4628            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4629            // trigger is a row-level AFTER trigger that additionally carries
4630            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4631            // path already tolerates and skips those clauses, so consuming the
4632            // CONSTRAINT keyword and reusing it makes the statement parse and the
4633            // trigger fire. (The deferral timing itself is not yet honoured —
4634            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4635            // for every non-deferred use.)
4636            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4637                self.advance();
4638                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4639                    if t.eq_ignore_ascii_case("trigger"))
4640                {
4641                    return Err(self.err(alloc::format!(
4642                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4643                        self.peek()
4644                    )));
4645                }
4646                self.advance();
4647                self.parse_create_trigger_after_keyword(false)
4648            }
4649            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4650            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4651                self.advance();
4652                self.parse_create_sequence_after_keyword(false)
4653            }
4654            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4655            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4656                self.advance();
4657                self.parse_create_view_after_keyword(false, false, false)
4658            }
4659            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4660            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4661            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4662            // appear (in any order) between `CREATE` and `VIEW` in
4663            // every mysqldump-emitted view. Pre-2.6 the parser
4664            // rejected the prefix and the customer's whole view
4665            // backup failed on the first view. The hints are pure
4666            // planner / permission metadata; SPG's view-rewrite
4667            // path is semantically equivalent for all three
4668            // algorithms in v7.17 (TEMPTABLE differs only in
4669            // perf for huge views — out of v7.17 scope), and
4670            // DEFINER / SQL SECURITY are pure single-user
4671            // permissioning that SPG ignores by design.
4672            Token::Ident(s) | Token::QuotedIdent(s)
4673                if s.eq_ignore_ascii_case("algorithm")
4674                    || s.eq_ignore_ascii_case("definer")
4675                    || s.eq_ignore_ascii_case("sql") =>
4676            {
4677                self.consume_mysql_view_prefix()?;
4678                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4679                // (in any order, in any combination), the next
4680                // keyword must be VIEW. mysqldump never emits these
4681                // prefixes on non-view statements.
4682                let next = self.peek().clone();
4683                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4684                    if s2.eq_ignore_ascii_case("view"))
4685                {
4686                    self.advance();
4687                    self.parse_create_view_after_keyword(false, false, false)
4688                } else {
4689                    Err(self.err(alloc::format!(
4690                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4691                    )))
4692                }
4693            }
4694            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4695            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4696                self.advance();
4697                self.parse_create_type_after_keyword()
4698            }
4699            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4700            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4701            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4702                self.advance();
4703                self.parse_create_domain_after_keyword()
4704            }
4705            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4706            // name [AUTHORIZATION user]. Real catalog registry
4707            // (was silent-no-op'd pre-v7.17).
4708            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4709                self.advance();
4710                let if_not_exists = self.parse_if_not_exists();
4711                let name = self.expect_ident_like()?;
4712                // Optional `AUTHORIZATION <user>` trailer — accepted,
4713                // ignored (single-user catalog).
4714                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4715                    if s.eq_ignore_ascii_case("authorization"))
4716                {
4717                    self.advance();
4718                    let _ = self.expect_ident_like()?;
4719                }
4720                Ok(Statement::CreateSchema { name, if_not_exists })
4721            }
4722            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4723            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4724                self.advance();
4725                let next = self.peek().clone();
4726                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4727                {
4728                    self.advance();
4729                    self.parse_create_materialized_view_after_keyword()
4730                } else {
4731                    Err(self.err(alloc::format!(
4732                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4733                    )))
4734                }
4735            }
4736            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4737            // no-op below), an UNLOGGED table is a real, fully-usable table in
4738            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4739            // durability optimisation is a follow-up), so a dump / app that
4740            // declares UNLOGGED tables works instead of failing to parse.
4741            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4742                self.advance(); // UNLOGGED
4743                if matches!(self.peek(), Token::Table) {
4744                    self.parse_create_table_stmt_after_create()
4745                } else {
4746                    Err(self.err(format!(
4747                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4748                        self.peek()
4749                    )))
4750                }
4751            }
4752            Token::Ident(s) | Token::QuotedIdent(s)
4753                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4754            {
4755                self.advance();
4756                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4757                let next = self.peek().clone();
4758                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4759                {
4760                    self.advance();
4761                    self.parse_create_sequence_after_keyword(true)
4762                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4763                {
4764                    self.advance();
4765                    self.parse_create_view_after_keyword(false, false, true)
4766                } else {
4767                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4768                    // consumed and answered OK while creating nothing, so
4769                    // every statement that touched the table afterwards failed
4770                    // with "table not found" — the DDL itself lied. It is a
4771                    // real CREATE TABLE now, marked temporary so the executor
4772                    // puts it in the session's own namespace. An optional
4773                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4774                    // is not legal, but the keyword is consumed by the
4775                    // CREATE TABLE parser itself).
4776                    let stmt = self.parse_create_table_stmt_after_create()?;
4777                    match stmt {
4778                        Statement::CreateTable(mut c) => {
4779                            c.temporary = true;
4780                            Ok(Statement::CreateTable(c))
4781                        }
4782                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4783                        // CTAS node, which needs the same session namespace.
4784                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4785                            m.temporary = true;
4786                            Ok(Statement::CreateMaterializedView(m))
4787                        }
4788                        other => Ok(other),
4789                    }
4790                }
4791            }
4792            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4793            // BEGIN <body> END`. The body may reference `@var`
4794            // session variables, SET statements, internal `;`
4795            // terminators, etc. SPG has no procedure runtime, so
4796            // consume the whole `CREATE PROCEDURE … END` block as
4797            // a no-op so mysqldump scripts that include stored
4798            // routines load through. The matching-END consumer
4799            // tracks BEGIN/END nesting depth to handle nested
4800            // BEGIN blocks correctly.
4801            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4802                self.consume_mysql_routine_body();
4803                Ok(Statement::Empty)
4804            }
4805            // v7.14.0 — pg_dump / mysqldump emit
4806            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4807            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4808            // SPG is single-schema / single-database; these have
4809            // no behavioural effect, so consume + return Empty.
4810            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4811            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4812            // moved up to real parser branches. DATABASE / ROLE /
4813            // POLICY / OPERATOR stay no-op forever
4814            // (single-database, hardcoded roles).
4815            Token::Ident(s) | Token::QuotedIdent(s)
4816                if matches!(
4817                    s.to_ascii_lowercase().as_str(),
4818                    "database"
4819                        | "role"
4820                        | "operator"
4821                        | "cast"
4822                        | "aggregate"
4823                        | "language"
4824                        | "collation"
4825                        | "conversion"
4826                        // v7.17.0 Phase 8 (audit N6) — rarely-
4827                        // emitted pg_dump shapes that should
4828                        // load through without a parser error.
4829                        // SPG has no planner statistics catalog,
4830                        // no event-trigger hooks, no foreign-
4831                        // data-wrapper infrastructure; consume
4832                        // + return Empty.
4833                        | "statistics"
4834                        | "event"
4835                        // v7.37.17 (17.6 siblings) — additional CREATE
4836                        // targets pg_dump / operator install scripts
4837                        // may emit that SPG has no matching machinery
4838                        // for. Consume + Empty-return.
4839                        | "text"
4840                        | "tablespace"
4841                        | "access"
4842                        | "large"
4843                ) =>
4844            {
4845                // DATABASE is the one member of this list PG refuses
4846                // inside a transaction block; the rest (ROLE, CAST,
4847                // TABLESPACE, …) it runs there quite happily, so only
4848                // this one is named. Still a no-op otherwise — SPG is
4849                // single-database.
4850                let is_database = s.eq_ignore_ascii_case("database");
4851                self.consume_until_statement_boundary();
4852                if is_database {
4853                    return Ok(Statement::NoOpPreventedInTransaction {
4854                        what: String::from("CREATE DATABASE"),
4855                    });
4856                }
4857                Ok(Statement::Empty)
4858            }
4859            // v7.39 (round 706) — the foreign-data family leaves the silent
4860            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4861            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4862            // FDW machinery), but the ENGINE now warns, so a restore log
4863            // says what will not function instead of reporting success.
4864            Token::Ident(s) | Token::QuotedIdent(s)
4865                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4866            {
4867                self.consume_until_statement_boundary();
4868                Ok(Statement::ValidateOnly {
4869                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4870                    names: Vec::new(),
4871                })
4872            }
4873            other => Err(self.err(format!(
4874                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4875            ))),
4876        }
4877    }
4878
4879    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4880    /// keyword decides whether we parse a function or trigger
4881    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4882    /// PROCEDURE) — those land in later releases.
4883    fn parse_create_function_or_trigger_after_or_replace(
4884        &mut self,
4885        or_replace: bool,
4886    ) -> Result<Statement, ParseError> {
4887        let tok = self.peek();
4888        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4889            return Err(self.err(alloc::format!(
4890                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4891            )));
4892        };
4893        if s.eq_ignore_ascii_case("function") {
4894            self.advance();
4895            self.parse_create_function_after_keyword(or_replace)
4896        } else if s.eq_ignore_ascii_case("trigger") {
4897            self.advance();
4898            self.parse_create_trigger_after_keyword(or_replace)
4899        } else if s.eq_ignore_ascii_case("rule") {
4900            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4901            self.advance();
4902            self.parse_create_rule_after_keyword(or_replace)
4903        } else if s.eq_ignore_ascii_case("view") {
4904            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4905            self.advance();
4906            self.parse_create_view_after_keyword(or_replace, false, false)
4907        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4908            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4909            self.advance();
4910            let nxt = self.peek().clone();
4911            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4912            {
4913                self.advance();
4914                self.parse_create_view_after_keyword(or_replace, false, true)
4915            } else {
4916                Err(self.err(alloc::format!(
4917                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4918                )))
4919            }
4920        } else {
4921            Err(self.err(alloc::format!(
4922                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4923            )))
4924        }
4925    }
4926
4927    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4928    /// SPG doesn't have a registry; pgvector / similar are
4929    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4930    /// the syntax lets dual-target schemas keep the line.
4931    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4932        // Optional `IF NOT EXISTS`.
4933        self.consume_if_not_exists();
4934        let name = self.expect_ident_like()?;
4935        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4936        // CASCADE / FROM '<v>' clauses; we don't model them.
4937        loop {
4938            match self.peek() {
4939                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4940                    self.advance();
4941                    continue;
4942                }
4943                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4944                    self.advance();
4945                    let _ = self.expect_ident_like()?;
4946                    continue;
4947                }
4948                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4949                    self.advance();
4950                    // String or ident literal.
4951                    let _ = self.advance();
4952                    continue;
4953                }
4954                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4955                    self.advance();
4956                    let _ = self.advance();
4957                    continue;
4958                }
4959                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4960                    self.advance();
4961                    continue;
4962                }
4963                _ => break,
4964            }
4965        }
4966        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4967        // nosuch` reported success and `pg_extension` then did not list it,
4968        // which is the accept-and-do-nothing shape F31 exists to find.
4969        Ok(Statement::ValidateOnly {
4970            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4971            names: alloc::vec![name],
4972        })
4973    }
4974
4975    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4976    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4977    /// already been consumed by the caller. Grammar accepted:
4978    ///
4979    ///   name `(` arg-list `)`
4980    ///   `RETURNS` return-type
4981    ///   [ `LANGUAGE` ident ]
4982    ///   `AS` $$ body $$
4983    ///   [ `LANGUAGE` ident ]
4984    ///
4985    /// Either `LANGUAGE` position is allowed; PG accepts both.
4986    fn parse_create_function_after_keyword(
4987        &mut self,
4988        or_replace: bool,
4989    ) -> Result<Statement, ParseError> {
4990        let name = self.expect_ident_like()?;
4991        // Argument list. v7.12.4 commonly sees the empty `()`
4992        // (trigger functions); typed args parse and round-trip
4993        // but the executor only invokes nullary functions.
4994        if !matches!(self.peek(), Token::LParen) {
4995            return Err(self.err(alloc::format!(
4996                "expected '(' after function name {name:?}, got {:?}",
4997                self.peek()
4998            )));
4999        }
5000        self.advance();
5001        let args = self.parse_function_arg_list()?;
5002        // RETURNS clause.
5003        let tok = self.peek();
5004        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5005            return Err(self.err(alloc::format!(
5006                "expected RETURNS after function arg list, got {tok:?}"
5007            )));
5008        };
5009        if !s.eq_ignore_ascii_case("returns") {
5010            return Err(self.err(alloc::format!(
5011                "expected RETURNS after function arg list, got {s:?}"
5012            )));
5013        }
5014        self.advance();
5015        let returns = self.parse_function_return()?;
5016        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5017        // re-check after the body too).
5018        let mut language: Option<String> = self.parse_optional_language()?;
5019        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5020        // either side of the body and in any order, interleaved with
5021        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5022        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5023        // PG's own pg_dump output did not restore.
5024        let mut attrs = FunctionAttrs::default();
5025        loop {
5026            let before = self.pos;
5027            self.parse_function_attrs_into(&mut attrs)?;
5028            if language.is_none() {
5029                language = self.parse_optional_language()?;
5030            }
5031            if self.pos == before {
5032                break;
5033            }
5034        }
5035        // `AS` followed by a $$-quoted body (lexer already
5036        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5037        // Token::String). AS is a reserved keyword (Token::As).
5038        if !matches!(self.peek(), Token::As) {
5039            return Err(self.err(alloc::format!(
5040                "expected AS before function body, got {:?}",
5041                self.peek()
5042            )));
5043        }
5044        self.advance();
5045        let body_text = match self.peek() {
5046            Token::String(s) => {
5047                let body = s.clone();
5048                self.advance();
5049                body
5050            }
5051            other => {
5052                return Err(self.err(alloc::format!(
5053                    "expected $$-quoted function body after AS, got {other:?}"
5054                )));
5055            }
5056        };
5057        // Trailing clauses — PG's other accepted position for both the
5058        // LANGUAGE and the attributes.
5059        loop {
5060            let before = self.pos;
5061            self.parse_function_attrs_into(&mut attrs)?;
5062            if language.is_none() {
5063                language = self.parse_optional_language()?;
5064            }
5065            if self.pos == before {
5066                break;
5067            }
5068        }
5069        let language = language.unwrap_or_else(|| String::from("sql"));
5070        // PL/pgSQL bodies get structure-parsed. Other languages
5071        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5072        // recognise) round-trip as Raw text — the executor errors
5073        // when invoked with a clear unsupported message.
5074        let body = if language.eq_ignore_ascii_case("plpgsql") {
5075            match parse_plpgsql_body(&body_text) {
5076                Ok(block) => FunctionBody::PlPgSql(block),
5077                // Best-effort: if the body parser doesn't yet
5078                // support a construct used inside, fall back to
5079                // raw — keeps `CREATE FUNCTION` itself working
5080                // (catalogue accepts), executor errors on
5081                // invocation only.
5082                Err(_) => FunctionBody::Raw(body_text),
5083            }
5084        } else {
5085            FunctionBody::Raw(body_text)
5086        };
5087        Ok(Statement::CreateFunction(CreateFunctionStatement {
5088            name,
5089            or_replace,
5090            args,
5091            returns,
5092            language,
5093            body,
5094            attrs,
5095        }))
5096    }
5097
5098    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5099    /// attribute clauses into `attrs`, stopping at the first token that
5100    /// is not one. Measured against PG 18.4, which accepts them in any
5101    /// order and on either side of the body.
5102    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5103        loop {
5104            let word = match self.peek() {
5105                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5106                // NOT LEAKPROOF — NOT is a reserved keyword token.
5107                Token::Not
5108                    if matches!(
5109                        self.tokens.get(self.pos + 1),
5110                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5111                    ) =>
5112                {
5113                    self.advance();
5114                    self.advance();
5115                    attrs.leakproof = false;
5116                    continue;
5117                }
5118                _ => return Ok(()),
5119            };
5120            match word.as_str() {
5121                "immutable" => {
5122                    self.advance();
5123                    attrs.volatility = FunctionVolatility::Immutable;
5124                }
5125                "stable" => {
5126                    self.advance();
5127                    attrs.volatility = FunctionVolatility::Stable;
5128                }
5129                "volatile" => {
5130                    self.advance();
5131                    attrs.volatility = FunctionVolatility::Volatile;
5132                }
5133                "strict" => {
5134                    self.advance();
5135                    attrs.strict = true;
5136                }
5137                "leakproof" => {
5138                    self.advance();
5139                    attrs.leakproof = true;
5140                }
5141                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5142                // spelled-out forms of STRICT and its opposite.
5143                "returns" | "called" => {
5144                    let strict = word == "returns";
5145                    let mut probe = self.pos + 1;
5146                    if strict {
5147                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5148                        // is not ours.
5149                        match self.tokens.get(probe) {
5150                            Some(Token::Null) => probe += 1,
5151                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5152                            _ => return Ok(()),
5153                        }
5154                    }
5155                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5156                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5157                    if !ok {
5158                        return Ok(());
5159                    }
5160                    probe += 1;
5161                    match self.tokens.get(probe) {
5162                        Some(Token::Null) => probe += 1,
5163                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5164                        _ => return Ok(()),
5165                    }
5166                    match self.tokens.get(probe) {
5167                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5168                        _ => return Ok(()),
5169                    }
5170                    self.pos = probe;
5171                    attrs.strict = strict;
5172                }
5173                "security" | "external" => {
5174                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5175                    let mut probe = self.pos + 1;
5176                    if word == "external" {
5177                        match self.tokens.get(probe) {
5178                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5179                                probe += 1;
5180                            }
5181                            _ => return Ok(()),
5182                        }
5183                    }
5184                    let definer = match self.tokens.get(probe) {
5185                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5186                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5187                        _ => return Ok(()),
5188                    };
5189                    self.pos = probe + 1;
5190                    attrs.security_definer = definer;
5191                }
5192                "parallel" => {
5193                    let level = match self.tokens.get(self.pos + 1) {
5194                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5195                            FunctionParallel::Safe
5196                        }
5197                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5198                            FunctionParallel::Restricted
5199                        }
5200                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5201                            FunctionParallel::Unsafe
5202                        }
5203                        _ => return Ok(()),
5204                    };
5205                    self.pos += 2;
5206                    attrs.parallel = level;
5207                }
5208                "cost" | "rows" => {
5209                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5210                        return Ok(());
5211                    };
5212                    self.pos += 2;
5213                    if word == "cost" {
5214                        attrs.cost = Some(n);
5215                    } else {
5216                        attrs.rows = Some(n);
5217                    }
5218                }
5219                _ => return Ok(()),
5220            }
5221        }
5222    }
5223
5224    /// The numeric literal at `idx`, if there is one.
5225    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5226        match self.tokens.get(idx)? {
5227            Token::Integer(n) => Some(*n as f64),
5228            Token::Float(f) => Some(*f),
5229            Token::Numeric(t) => t.parse::<f64>().ok(),
5230            _ => None,
5231        }
5232    }
5233
5234    /// Closing `)`-terminated argument list. v7.12.4 commonly
5235    /// sees the empty `()`; typed args round-trip but the
5236    /// executor (yet) doesn't invoke them.
5237    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5238    /// it away, which is what PG does with one on a function parameter.
5239    fn skip_type_modifier(&mut self) {
5240        if !matches!(self.peek(), Token::LParen) {
5241            return;
5242        }
5243        // Only a numeric modifier — anything else is not one, and eating
5244        // it would swallow real grammar.
5245        let mut i = self.pos + 1;
5246        let mut seen_number = false;
5247        loop {
5248            match self.tokens.get(i) {
5249                Some(Token::Integer(_)) => seen_number = true,
5250                Some(Token::Comma) => {}
5251                Some(Token::RParen) => break,
5252                _ => return,
5253            }
5254            i += 1;
5255        }
5256        if !seen_number {
5257            return;
5258        }
5259        while self.pos <= i {
5260            self.advance();
5261        }
5262    }
5263
5264    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5265        let mut args: Vec<FunctionArg> = Vec::new();
5266        if matches!(self.peek(), Token::RParen) {
5267            self.advance();
5268            return Ok(args);
5269        }
5270        loop {
5271            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5272            // a reserved token; OUT / INOUT are bare idents.
5273            let mode = if matches!(self.peek(), Token::In) {
5274                self.advance();
5275                FunctionArgMode::In
5276            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5277            {
5278                self.advance();
5279                FunctionArgMode::Out
5280            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5281            {
5282                self.advance();
5283                FunctionArgMode::InOut
5284            } else {
5285                FunctionArgMode::In
5286            };
5287            // Optional name. The next token is either a name
5288            // (followed by a type ident) or the type itself.
5289            // Disambiguate by peeking ahead: if the token after
5290            // the next ident is also an ident, we treat the
5291            // first as the name.
5292            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5293            // the comma or paren, then decide. Reading at most two of
5294            // them could not spell `x double precision` at all, and
5295            // silently mis-read the bare `double precision` as a
5296            // parameter named "double" — which is what made the same
5297            // signature key two different ways.
5298            let (name, ty_token) = {
5299                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5300                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5301                    words.push(self.expect_ident_like()?);
5302                }
5303                // v7.39 (round 344) — a length / precision modifier on the
5304                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5305                // accepts it and DROPS it — `pg_get_function_arguments`
5306                // reports plain `character varying` / `numeric`, measured on
5307                // 18.4 — but SPG raised `syntax error at or near "("`,
5308                // because the modifier's parens were never consumed.
5309                self.skip_type_modifier();
5310                // r1049 — `f(v bigint[])`. The array suffix parsed in
5311                // the column position, the cast position and (r1038)
5312                // the RETURNS position, but not here: the fifth
5313                // member of the same family, reported by sentori as
5314                // presumably the same code. It is now.
5315                let array_suffix = self.consume_array_suffix();
5316                let whole = words.join(" ");
5317                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5318                {
5319                    (Some(words[0].clone()), words[1..].join(" "))
5320                } else {
5321                    (None, whole)
5322                };
5323                ty_token.push_str(&array_suffix);
5324                (name, ty_token)
5325            };
5326            // Type — try to map to ColumnTypeName, else Raw.
5327            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5328                Some(t) => FunctionArgType::Typed(t),
5329                None => FunctionArgType::Raw(ty_token),
5330            };
5331            args.push(FunctionArg { mode, name, ty });
5332            match self.peek() {
5333                Token::Comma => {
5334                    self.advance();
5335                    continue;
5336                }
5337                Token::RParen => {
5338                    self.advance();
5339                    return Ok(args);
5340                }
5341                other => {
5342                    return Err(self.err(alloc::format!(
5343                        "expected , or ) in function arg list, got {other:?}"
5344                    )));
5345                }
5346            }
5347        }
5348    }
5349
5350    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5351        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5352        // function whose row shape is named inline.
5353        if matches!(self.peek(), Token::Table)
5354            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5355        {
5356            self.advance(); // TABLE
5357            self.advance(); // (
5358            let mut cols: Vec<String> = Vec::new();
5359            loop {
5360                let cname = self.expect_ident_like()?;
5361                let mut ty: Vec<String> = Vec::new();
5362                loop {
5363                    match self.peek() {
5364                        Token::Comma | Token::RParen | Token::Eof => break,
5365                        _ => {}
5366                    }
5367                    match self.advance() {
5368                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5369                        other => {
5370                            if let Some(w) = unreserved_keyword_text(&other) {
5371                                ty.push(w);
5372                            }
5373                        }
5374                    }
5375                }
5376                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5377                if matches!(self.peek(), Token::Comma) {
5378                    self.advance();
5379                } else {
5380                    break;
5381                }
5382            }
5383            if matches!(self.peek(), Token::RParen) {
5384                self.advance();
5385            }
5386            return Ok(FunctionReturn::Other(alloc::format!(
5387                "TABLE({})",
5388                cols.join(", ")
5389            )));
5390        }
5391        let ident = self.expect_ident_like()?;
5392        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5393        if ident.eq_ignore_ascii_case("setof") {
5394            let inner = self.expect_ident_like()?;
5395            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5396            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5397        }
5398        if ident.eq_ignore_ascii_case("trigger") {
5399            return Ok(FunctionReturn::Trigger);
5400        }
5401        if ident.eq_ignore_ascii_case("void") {
5402            return Ok(FunctionReturn::Void);
5403        }
5404        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5405        // RETURN position did not, so the `[` was a syntax error and the
5406        // whole migration stopped. sentori worked around it by returning
5407        // zero-padded text.
5408        let suffix = self.consume_array_suffix();
5409        if !suffix.is_empty() {
5410            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5411        }
5412        match map_type_ident_to_column_type_name(&ident) {
5413            Some(t) => Ok(FunctionReturn::Type(t)),
5414            None => Ok(FunctionReturn::Other(ident)),
5415        }
5416    }
5417
5418    /// Consume any `[]` / `[N]` array markers after a type name and give
5419    /// back their text. Empty when there are none.
5420    fn consume_array_suffix(&mut self) -> String {
5421        let mut out = String::new();
5422        while matches!(self.peek(), Token::LBracket) {
5423            self.advance();
5424            // `[N]` is accepted and, as in PG, the length is not enforced.
5425            if let Token::Integer(n) = self.peek().clone() {
5426                self.advance();
5427                out.push_str(&alloc::format!("[{n}]"));
5428            } else {
5429                out.push_str("[]");
5430            }
5431            if matches!(self.peek(), Token::RBracket) {
5432                self.advance();
5433            }
5434        }
5435        out
5436    }
5437
5438    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5439        match self.peek() {
5440            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5441                self.advance();
5442                let lang = self.expect_ident_like()?;
5443                Ok(Some(lang.to_ascii_lowercase()))
5444            }
5445            _ => Ok(None),
5446        }
5447    }
5448
5449    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5450    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5451    /// (expr)]*`. The `DOMAIN` keyword has already been
5452    /// consumed. PG allows the trailing constraints in any
5453    /// order; we approximate with a small loop.
5454    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5455        let name = self.expect_ident_like()?;
5456        // Optional `AS`.
5457        if matches!(self.peek(), Token::As) {
5458            self.advance();
5459        }
5460        // v7.39 (round 259) — keep the raw type NAME when the base is not
5461        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5462        // parent domain.
5463        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5464            self.parse_type_with_implied_flags()?;
5465        let mut default: Option<Expr> = None;
5466        let mut not_null = false;
5467        let mut checks: Vec<Expr> = Vec::new();
5468        loop {
5469            match self.peek() {
5470                Token::Default => {
5471                    if default.is_some() {
5472                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5473                    }
5474                    self.advance();
5475                    default = Some(self.parse_expr(0)?);
5476                }
5477                Token::Not => {
5478                    self.advance();
5479                    if !matches!(self.peek(), Token::Null) {
5480                        return Err(self.err(alloc::format!(
5481                            "expected NULL after NOT in DOMAIN, got {:?}",
5482                            self.peek()
5483                        )));
5484                    }
5485                    self.advance();
5486                    not_null = true;
5487                }
5488                Token::Null => {
5489                    self.advance();
5490                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5491                    // is the default-nullable marker (PG accepts it),
5492                    // but AFTER a NOT NULL it is a conflict PG refuses
5493                    // (`conflicting NULL/NOT NULL constraints`,
5494                    // PG18-measured); the old arm no-opped both ways.
5495                    if not_null {
5496                        return Err(self.err(alloc::string::String::from(
5497                            "conflicting NULL/NOT NULL constraints",
5498                        )));
5499                    }
5500                }
5501                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5502                    self.advance();
5503                    if !matches!(self.peek(), Token::LParen) {
5504                        return Err(self.err(alloc::format!(
5505                            "expected '(' after CHECK in DOMAIN, got {:?}",
5506                            self.peek()
5507                        )));
5508                    }
5509                    self.advance();
5510                    let expr = self.parse_expr(0)?;
5511                    if !matches!(self.peek(), Token::RParen) {
5512                        return Err(self.err(alloc::format!(
5513                            "expected ')' after CHECK expr, got {:?}",
5514                            self.peek()
5515                        )));
5516                    }
5517                    self.advance();
5518                    checks.push(expr);
5519                }
5520                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5521                // prefix on the constraint; we drop the name and
5522                // recurse into the constraint parsing.
5523                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5524                    self.advance();
5525                    let _ = self.expect_ident_like()?;
5526                }
5527                _ => break,
5528            }
5529        }
5530        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5531            name,
5532            base_type,
5533            base_domain: base_user_ref,
5534            default,
5535            not_null,
5536            checks,
5537        }))
5538    }
5539
5540    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5541    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5542    /// consumed.
5543    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5544        let name = self.expect_ident_like()?;
5545        // Required `AS`.
5546        if !matches!(self.peek(), Token::As) {
5547            return Err(self.err(alloc::format!(
5548                "expected AS after CREATE TYPE {name:?}, got {:?}",
5549                self.peek()
5550            )));
5551        }
5552        self.advance();
5553        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5554        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5555        // on the next token: `(` = composite, ident `ENUM` = enum.
5556        if matches!(self.peek(), Token::LParen) {
5557            self.advance();
5558            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5559            let mut field_user_types: Vec<Option<String>> = Vec::new();
5560            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5561            // is legal PG (an attribute-less composite; measured — the old
5562            // e2e note claimed PG requires at least one attribute).
5563            if matches!(self.peek(), Token::RParen) {
5564                self.advance();
5565                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5566                    name,
5567                    kind: crate::ast::TypeKind::Composite {
5568                        fields,
5569                        field_user_types,
5570                    },
5571                }));
5572            }
5573            loop {
5574                let field_name = self.expect_ident_like()?;
5575                // v7.39 (round 264) — keep the raw type name when it is not
5576                // a builtin: that is how a NESTED composite field records
5577                // which composite it holds.
5578                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5579                    self.parse_type_with_implied_flags()?;
5580                fields.push((field_name, field_type));
5581                field_user_types.push(field_user_ref);
5582                if matches!(self.peek(), Token::Comma) {
5583                    self.advance();
5584                    continue;
5585                }
5586                if matches!(self.peek(), Token::RParen) {
5587                    self.advance();
5588                    break;
5589                }
5590                return Err(self.err(alloc::format!(
5591                    "expected , or ) in composite field list, got {:?}",
5592                    self.peek()
5593                )));
5594            }
5595            if fields.is_empty() {
5596                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5597            }
5598            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5599                name,
5600                kind: crate::ast::TypeKind::Composite {
5601                    fields,
5602                    field_user_types,
5603                },
5604            }));
5605        }
5606        // Required `ENUM` ident.
5607        let kind_ident = match self.peek().clone() {
5608            Token::Ident(s) | Token::QuotedIdent(s) => s,
5609            other => {
5610                return Err(self.err(alloc::format!(
5611                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5612                )));
5613            }
5614        };
5615        if !kind_ident.eq_ignore_ascii_case("enum") {
5616            return Err(self.err(alloc::format!(
5617                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5618            )));
5619        }
5620        self.advance();
5621        if !matches!(self.peek(), Token::LParen) {
5622            return Err(self.err(alloc::format!(
5623                "expected '(' after ENUM, got {:?}",
5624                self.peek()
5625            )));
5626        }
5627        self.advance();
5628        let mut labels: Vec<String> = Vec::new();
5629        loop {
5630            match self.peek().clone() {
5631                Token::String(s) => {
5632                    self.advance();
5633                    labels.push(s);
5634                }
5635                other => {
5636                    return Err(
5637                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5638                    );
5639                }
5640            }
5641            if matches!(self.peek(), Token::Comma) {
5642                self.advance();
5643                continue;
5644            }
5645            if matches!(self.peek(), Token::RParen) {
5646                self.advance();
5647                break;
5648            }
5649            return Err(self.err(alloc::format!(
5650                "expected , or ) in ENUM label list, got {:?}",
5651                self.peek()
5652            )));
5653        }
5654        if labels.is_empty() {
5655            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5656        }
5657        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5658            name,
5659            kind: crate::ast::TypeKind::Enum { labels },
5660        }))
5661    }
5662
5663    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5664    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5665    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5666    /// consumed.
5667    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5668        let if_not_exists = self.parse_if_not_exists();
5669        let name = self.expect_ident_like()?;
5670        let mut columns: Vec<String> = Vec::new();
5671        if matches!(self.peek(), Token::LParen) {
5672            self.advance();
5673            loop {
5674                let c = self.expect_ident_like()?;
5675                columns.push(c);
5676                if matches!(self.peek(), Token::Comma) {
5677                    self.advance();
5678                    continue;
5679                }
5680                if matches!(self.peek(), Token::RParen) {
5681                    self.advance();
5682                    break;
5683                }
5684                return Err(self.err(alloc::format!(
5685                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5686                    self.peek()
5687                )));
5688            }
5689        }
5690        if !matches!(self.peek(), Token::As) {
5691            return Err(self.err(alloc::format!(
5692                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5693                self.peek()
5694            )));
5695        }
5696        self.advance();
5697        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5698        // CTEs only; the engine rejects data-modifying ones with PG's
5699        // message). A trailing `WITH [NO] DATA` can't START the body,
5700        // so WITH here heads the query.
5701        let body = if self.peek_is_with_kw() {
5702            self.advance();
5703            self.parse_nested_with_select()?
5704        } else {
5705            let body_stmt = self.parse_select_stmt()?;
5706            let Statement::Select(body) = body_stmt else {
5707                return Err(self.err(alloc::format!(
5708                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5709                )));
5710            };
5711            body
5712        };
5713        // Optional trailing `WITH [NO] DATA`.
5714        let with_data = self.parse_optional_with_data(true)?;
5715        Ok(Statement::CreateMaterializedView(
5716            crate::ast::CreateMaterializedViewStatement {
5717                temporary: false,
5718                name,
5719                if_not_exists,
5720                columns,
5721                body,
5722                with_data,
5723                as_plain_table: false,
5724            },
5725        ))
5726    }
5727
5728    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5729    /// `default_when_absent` is what to return if the tail is
5730    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5731    /// WITH DATA).
5732    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5733        let save = self.pos;
5734        // `WITH` is an Ident (not reserved in the lexer).
5735        let is_with = match self.peek() {
5736            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5737            _ => false,
5738        };
5739        if !is_with {
5740            return Ok(default_when_absent);
5741        }
5742        self.advance();
5743        // Optional `NO`.
5744        let mut with_data = true;
5745        let is_no = match self.peek() {
5746            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5747            _ => false,
5748        };
5749        if is_no {
5750            self.advance();
5751            with_data = false;
5752        }
5753        // Required `DATA` ident.
5754        let is_data = match self.peek() {
5755            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5756            _ => false,
5757        };
5758        if is_data {
5759            self.advance();
5760            Ok(with_data)
5761        } else {
5762            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5763            // parser can interpret it.
5764            self.pos = save;
5765            Ok(default_when_absent)
5766        }
5767    }
5768
5769    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5770    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5771    /// All keyword prefixes have already been consumed; the flags
5772    /// say which were present.
5773    fn parse_create_view_after_keyword(
5774        &mut self,
5775        or_replace: bool,
5776        _materialized_unused: bool,
5777        temporary: bool,
5778    ) -> Result<Statement, ParseError> {
5779        let if_not_exists = self.parse_if_not_exists();
5780        let name = self.expect_ident_like()?;
5781        // Optional `(col, col, …)` rename list.
5782        let mut columns: Vec<String> = Vec::new();
5783        if matches!(self.peek(), Token::LParen) {
5784            self.advance();
5785            loop {
5786                let c = self.expect_ident_like()?;
5787                columns.push(c);
5788                if matches!(self.peek(), Token::Comma) {
5789                    self.advance();
5790                    continue;
5791                }
5792                if matches!(self.peek(), Token::RParen) {
5793                    self.advance();
5794                    break;
5795                }
5796                return Err(self.err(alloc::format!(
5797                    "expected , or ) in VIEW column list, got {:?}",
5798                    self.peek()
5799                )));
5800            }
5801        }
5802        // Required `AS`.
5803        if !matches!(self.peek(), Token::As) {
5804            return Err(self.err(alloc::format!(
5805                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5806                self.peek()
5807            )));
5808        }
5809        self.advance();
5810        // Body: a regular SELECT statement. v7.39 (round 151) — a
5811        // WITH-headed body is legal too (read-only CTEs only; the
5812        // engine rejects data-modifying ones with PG's message).
5813        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5814        // with the check-option clause, so WITH here heads the query.
5815        let body = if self.peek_is_with_kw() {
5816            self.advance();
5817            self.parse_nested_with_select()?
5818        } else {
5819            let body_stmt = self.parse_select_stmt()?;
5820            let Statement::Select(body) = body_stmt else {
5821                return Err(self.err(alloc::format!(
5822                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5823                )));
5824            };
5825            body
5826        };
5827        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5828        // The SELECT parser stops before a trailing WITH, so it lands here.
5829        let check_option = if matches!(self.peek(),
5830            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5831        {
5832            self.advance(); // WITH
5833            let opt = match self.peek() {
5834                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5835                    self.advance();
5836                    crate::ast::ViewCheckOption::Local
5837                }
5838                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5839                    self.advance();
5840                    crate::ast::ViewCheckOption::Cascaded
5841                }
5842                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5843                _ => crate::ast::ViewCheckOption::Cascaded,
5844            };
5845            if !matches!(self.peek(),
5846                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5847            {
5848                return Err(self.err(alloc::format!(
5849                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5850                    self.peek()
5851                )));
5852            }
5853            self.advance(); // CHECK
5854            if !matches!(self.peek(),
5855                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5856            {
5857                return Err(self.err(alloc::format!(
5858                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5859                    self.peek()
5860                )));
5861            }
5862            self.advance(); // OPTION
5863            Some(opt)
5864        } else {
5865            None
5866        };
5867        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5868            name,
5869            or_replace,
5870            if_not_exists,
5871            temporary,
5872            columns,
5873            body,
5874            check_option,
5875        }))
5876    }
5877
5878    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5879    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5880    /// consumed; `temporary` carries whether TEMPORARY was seen.
5881    fn parse_create_sequence_after_keyword(
5882        &mut self,
5883        temporary: bool,
5884    ) -> Result<Statement, ParseError> {
5885        let if_not_exists = self.parse_if_not_exists();
5886        let name = self.expect_ident_like()?;
5887        // Optional `AS data_type`.
5888        let data_type = if matches!(self.peek(), Token::As) {
5889            self.advance();
5890            Some(self.parse_sequence_data_type()?)
5891        } else {
5892            None
5893        };
5894        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5895        Ok(Statement::CreateSequence(
5896            crate::ast::CreateSequenceStatement {
5897                name,
5898                if_not_exists,
5899                temporary,
5900                data_type,
5901                options,
5902            },
5903        ))
5904    }
5905
5906    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5907    /// already been consumed; this is reached after `SEQUENCE`.
5908    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5909    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5910        use crate::ast::AlterDomainAction as A;
5911        let name = self.expect_ident_like()?;
5912        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5913        let kw = match self.peek() {
5914            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5915            Token::Drop => alloc::string::String::from("drop"),
5916            Token::Default => alloc::string::String::from("default"),
5917            other => {
5918                return Err(self.err(alloc::format!(
5919                    "expected an ALTER DOMAIN action, got {other:?}"
5920                )));
5921            }
5922        };
5923        let action = match kw.as_str() {
5924            "add" => {
5925                self.advance();
5926                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5927                {
5928                    self.advance();
5929                    Some(self.expect_ident_like()?)
5930                } else {
5931                    None
5932                };
5933                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5934                    return Err(self.err(alloc::format!(
5935                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5936                        self.peek()
5937                    )));
5938                }
5939                self.advance();
5940                if !matches!(self.peek(), Token::LParen) {
5941                    return Err(self.err("expected '(' after CHECK".into()));
5942                }
5943                self.advance();
5944                let check = self.parse_expr(0)?;
5945                if !matches!(self.peek(), Token::RParen) {
5946                    return Err(self.err("expected ')' after CHECK expression".into()));
5947                }
5948                self.advance();
5949                A::AddConstraint { name: cname, check }
5950            }
5951            "drop" => {
5952                self.advance();
5953                match self.peek() {
5954                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5955                        self.advance();
5956                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5957                        {
5958                            self.advance();
5959                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5960                            {
5961                                return Err(self.err("expected EXISTS after IF".into()));
5962                            }
5963                            self.advance();
5964                            true
5965                        } else {
5966                            false
5967                        };
5968                        let cn = self.expect_ident_like()?;
5969                        A::DropConstraint {
5970                            name: cn,
5971                            if_exists,
5972                        }
5973                    }
5974                    Token::Default => {
5975                        self.advance();
5976                        A::DropDefault
5977                    }
5978                    Token::Not => {
5979                        self.advance();
5980                        if !matches!(self.peek(), Token::Null) {
5981                            return Err(self.err("expected NULL after NOT".into()));
5982                        }
5983                        self.advance();
5984                        A::DropNotNull
5985                    }
5986                    other => {
5987                        return Err(self.err(alloc::format!(
5988                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5989                        )));
5990                    }
5991                }
5992            }
5993            "set" => {
5994                self.advance();
5995                match self.peek() {
5996                    Token::Default => {
5997                        self.advance();
5998                        A::SetDefault(self.parse_expr(0)?)
5999                    }
6000                    Token::Not => {
6001                        self.advance();
6002                        if !matches!(self.peek(), Token::Null) {
6003                            return Err(self.err("expected NULL after NOT".into()));
6004                        }
6005                        self.advance();
6006                        A::SetNotNull
6007                    }
6008                    other => {
6009                        return Err(self.err(alloc::format!(
6010                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6011                        )));
6012                    }
6013                }
6014            }
6015            "rename" => {
6016                self.advance();
6017                if !matches!(self.peek(), Token::To) {
6018                    return Err(self.err("expected TO after RENAME".into()));
6019                }
6020                self.advance();
6021                A::RenameTo(self.expect_ident_like()?)
6022            }
6023            other => {
6024                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6025            }
6026        };
6027        Ok(Statement::AlterDomain { name, action })
6028    }
6029
6030    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6031        let if_exists = self.parse_if_exists();
6032        let name = self.expect_ident_like()?;
6033        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6034        // the option list (PG allows only one or the other).
6035        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6036            self.advance();
6037            if matches!(self.peek(), Token::To) {
6038                self.advance();
6039            } else {
6040                self.expect_keyword_ident("to")?;
6041            }
6042            let new = self.expect_ident_like()?;
6043            return Ok(Statement::AlterSequence(
6044                crate::ast::AlterSequenceStatement {
6045                    name,
6046                    if_exists,
6047                    options: crate::ast::SequenceOptions::default(),
6048                    rename_to: Some(new),
6049                },
6050            ));
6051        }
6052        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6053        Ok(Statement::AlterSequence(
6054            crate::ast::AlterSequenceStatement {
6055                name,
6056                if_exists,
6057                options,
6058                rename_to: None,
6059            },
6060        ))
6061    }
6062
6063    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6064        let kw = self.expect_ident_like()?;
6065        match kw.to_ascii_lowercase().as_str() {
6066            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6067            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6068            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6069            other => Err(self.err(alloc::format!(
6070                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6071            ))),
6072        }
6073    }
6074
6075    fn parse_sequence_options(
6076        &mut self,
6077        allow_restart: bool,
6078    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6079        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6080        let mut opts = SequenceOptions::default();
6081        #[allow(clippy::while_let_loop)]
6082        loop {
6083            // Match an ident; stop at any non-ident token (sentinel,
6084            // semicolon, end of statement).
6085            let kw_lc = match self.peek() {
6086                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6087                _ => break,
6088            };
6089            match kw_lc.as_str() {
6090                "increment" => {
6091                    self.advance();
6092                    // Optional BY.
6093                    if self.peek_is_by() {
6094                        self.advance();
6095                    }
6096                    opts.increment = Some(self.expect_signed_int()?);
6097                }
6098                "minvalue" => {
6099                    self.advance();
6100                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6101                }
6102                "maxvalue" => {
6103                    self.advance();
6104                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6105                }
6106                "no" => {
6107                    self.advance();
6108                    let what = self.expect_ident_like()?;
6109                    match what.to_ascii_lowercase().as_str() {
6110                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6111                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6112                        "cycle" => opts.cycle = Some(false),
6113                        other => {
6114                            return Err(self.err(alloc::format!(
6115                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6116                            )));
6117                        }
6118                    }
6119                }
6120                "start" => {
6121                    self.advance();
6122                    // Optional WITH.
6123                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6124                        if s.eq_ignore_ascii_case("with"))
6125                    {
6126                        self.advance();
6127                    }
6128                    opts.start = Some(self.expect_signed_int()?);
6129                }
6130                "restart" if allow_restart => {
6131                    self.advance();
6132                    // Optional WITH n; bare RESTART means restart at START.
6133                    let mut with_val: Option<i64> = None;
6134                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6135                        if s.eq_ignore_ascii_case("with"))
6136                    {
6137                        self.advance();
6138                        with_val = Some(self.expect_signed_int()?);
6139                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6140                        with_val = Some(self.expect_signed_int()?);
6141                    }
6142                    opts.restart = Some(with_val);
6143                }
6144                "cache" => {
6145                    self.advance();
6146                    opts.cache = Some(self.expect_signed_int()?);
6147                }
6148                "cycle" => {
6149                    self.advance();
6150                    opts.cycle = Some(true);
6151                }
6152                "owned" => {
6153                    self.advance();
6154                    match self.peek() {
6155                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6156                            self.advance();
6157                        }
6158                        other => {
6159                            return Err(
6160                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6161                            );
6162                        }
6163                    }
6164                    // OWNED BY {NONE | tab.col}. Read just one ident
6165                    // (NOT expect_ident_like which would auto-strip
6166                    // a schema prefix and consume the `.col` we need).
6167                    let first = match self.advance() {
6168                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6169                        other => {
6170                            return Err(self.err(alloc::format!(
6171                                "expected identifier or NONE after OWNED BY, got {other:?}"
6172                            )));
6173                        }
6174                    };
6175                    if first.eq_ignore_ascii_case("none") {
6176                        opts.owned_by = Some(SequenceOwnedBy::None);
6177                    } else if matches!(self.peek(), Token::Dot) {
6178                        self.advance();
6179                        let second = match self.advance() {
6180                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6181                            other => {
6182                                return Err(self.err(alloc::format!(
6183                                    "expected column name after OWNED BY {first}., got {other:?}"
6184                                )));
6185                            }
6186                        };
6187                        // v7.17 dump-compat fix — pg_dump emits
6188                        // OWNED BY clauses as
6189                        // `schema.table.column` (three segments).
6190                        // If a third `.<ident>` follows, treat the
6191                        // first ident as schema (drop it; SPG is
6192                        // single-schema) and the middle / last
6193                        // pair as table.column. Otherwise it's
6194                        // the two-segment form table.column.
6195                        if matches!(self.peek(), Token::Dot) {
6196                            self.advance();
6197                            let third = match self.advance() {
6198                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6199                                other => {
6200                                    return Err(self.err(alloc::format!(
6201                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6202                                    )));
6203                                }
6204                            };
6205                            let _ = first; // schema prefix discarded
6206                            opts.owned_by = Some(SequenceOwnedBy::Column {
6207                                table: second,
6208                                column: third,
6209                            });
6210                        } else {
6211                            opts.owned_by = Some(SequenceOwnedBy::Column {
6212                                table: first,
6213                                column: second,
6214                            });
6215                        }
6216                    } else {
6217                        return Err(self.err(alloc::format!(
6218                            "expected table.column or NONE after OWNED BY, got {first:?}"
6219                        )));
6220                    }
6221                }
6222                _ => break,
6223            }
6224        }
6225        Ok(opts)
6226    }
6227
6228    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6229        let neg = if matches!(self.peek(), Token::Minus) {
6230            self.advance();
6231            true
6232        } else {
6233            false
6234        };
6235        match self.peek() {
6236            Token::Integer(n) => {
6237                let v = *n;
6238                self.advance();
6239                Ok(if neg { -v } else { v })
6240            }
6241            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6242        }
6243    }
6244
6245    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6246    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6247    /// clause is fully accepted and discarded — SPG always runs
6248    /// constraint checks immediately (single-writer model). The
6249    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6250    /// in either order (per the SQL spec they're independent),
6251    /// though pg_dump always emits them in the canonical
6252    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6253    /// Stops at the first token that isn't part of the clause.
6254    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6255        self.consume_deferrable_clauses_timed().map(|_| ())
6256    }
6257
6258    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6259    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6260    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6261    /// NOT DEFERRABLE and a circular-FK migration could not load.
6262    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6263        let mut deferrable = false;
6264        let mut initially_deferred = false;
6265        loop {
6266            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6267            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6268                self.advance();
6269                deferrable = true;
6270                if self.consume_optional_initially_clause()? {
6271                    initially_deferred = true;
6272                }
6273                continue;
6274            }
6275            // `NOT DEFERRABLE` — already worked pre-3.1.
6276            if matches!(self.peek(), Token::Not) {
6277                let look = self.tokens.get(self.pos + 1);
6278                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6279                    self.advance(); // NOT
6280                    self.advance(); // DEFERRABLE
6281                    deferrable = false;
6282                    initially_deferred = false;
6283                    let _ = self.consume_optional_initially_clause()?;
6284                    continue;
6285                }
6286                break;
6287            }
6288            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6289            // accepts this without a leading [NOT] DEFERRABLE
6290            // (the timing keyword alone). pg_dump occasionally
6291            // emits it on FK constraints that inherit timing.
6292            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6293                if self.consume_optional_initially_clause()? {
6294                    initially_deferred = true;
6295                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6296                    deferrable = true;
6297                }
6298                continue;
6299            }
6300            break;
6301        }
6302        Ok((deferrable, initially_deferred))
6303    }
6304
6305    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6306    /// next token is `INITIALLY`, consume it plus the required
6307    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6308    /// Returns true when the timing seen was `DEFERRED`.
6309    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6310        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6311            return Ok(false);
6312        }
6313        self.advance(); // INITIALLY
6314        match self.advance() {
6315            Token::Ident(s)
6316                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6317            {
6318                Ok(s.eq_ignore_ascii_case("deferred"))
6319            }
6320            other => Err(self.err(alloc::format!(
6321                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6322            ))),
6323        }
6324    }
6325
6326    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6327    /// in its entirety so the parser returns Empty without
6328    /// touching the runtime. The CREATE+PROCEDURE keywords are
6329    /// already consumed; this swallows everything from the
6330    /// procedure name through the matching `END`, including
6331    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6332    /// (DELIMITER `//` makes the script splitter forward the
6333    /// whole block as one statement), `@var` session-variable
6334    /// references, and the trailing terminator.
6335    ///
6336    /// Tracks nesting depth so:
6337    ///   BEGIN
6338    ///     IF cond THEN
6339    ///       BEGIN ... END;
6340    ///     END IF;
6341    ///   END
6342    /// terminates at the outer END.
6343    fn consume_mysql_routine_body(&mut self) {
6344        // Outer skeleton: name, (...), optional clauses, BEGIN
6345        // <body> END [;]. Scan for the first BEGIN — anything
6346        // before it is signature decoration we don't care about.
6347        // Once inside BEGIN, count up on BEGIN, down on END.
6348        let mut depth: i32 = 0;
6349        let mut started = false;
6350        loop {
6351            match self.peek().clone() {
6352                Token::Begin => {
6353                    self.advance();
6354                    depth += 1;
6355                    started = true;
6356                }
6357                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6358                    self.advance();
6359                    if started {
6360                        depth -= 1;
6361                        if depth <= 0 {
6362                            // Optional trailing ident (`END IF`,
6363                            // `END LOOP`, `END WHILE`, `END CASE`,
6364                            // `END label_name`) — eat the next
6365                            // ident if present so we don't
6366                            // mistake `END IF;` for the outer
6367                            // close.
6368                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6369                                // If the next token is one of the
6370                                // PL/SQL block-closer keywords,
6371                                // the END belongs to an inner
6372                                // block; bump depth back up.
6373                                let is_inner_close = matches!(
6374                                    self.peek(),
6375                                    Token::Ident(s) | Token::QuotedIdent(s)
6376                                        if matches!(
6377                                            s.to_ascii_lowercase().as_str(),
6378                                            "if" | "loop" | "while" | "case" | "repeat"
6379                                        )
6380                                );
6381                                if is_inner_close {
6382                                    self.advance();
6383                                    depth += 1;
6384                                    continue;
6385                                }
6386                            }
6387                            // Eat optional trailing `;`.
6388                            if matches!(self.peek(), Token::Semicolon) {
6389                                self.advance();
6390                            }
6391                            return;
6392                        }
6393                    }
6394                }
6395                Token::Eof => return,
6396                _ => {
6397                    self.advance();
6398                }
6399            }
6400        }
6401    }
6402
6403    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6404    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6405    ///
6406    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6407    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6408    ///   ident, or `ident @ ident-or-quoted-string` host form)
6409    /// * `SQL SECURITY {DEFINER|INVOKER}`
6410    ///
6411    /// Each clause may appear at most once but in any order.
6412    /// The hints are pure planner / permission metadata that
6413    /// SPG's view-rewrite engine handles uniformly; we accept
6414    /// and discard. Returns `Ok(())` once a non-clause token is
6415    /// peeked (the caller then checks for the `VIEW` keyword).
6416    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6417        loop {
6418            match self.peek().clone() {
6419                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6420                    self.advance(); // ALGORITHM
6421                    // Optional `=`. MySQL spec requires it but be
6422                    // generous.
6423                    if matches!(self.peek(), Token::Eq) {
6424                        self.advance();
6425                    }
6426                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6427                    // bare ident; unknown values still parse so
6428                    // future MySQL versions don't break.
6429                    if matches!(
6430                        self.peek(),
6431                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6432                    ) {
6433                        self.advance();
6434                    }
6435                }
6436                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6437                    self.advance(); // DEFINER
6438                    if matches!(self.peek(), Token::Eq) {
6439                        self.advance();
6440                    }
6441                    // User: quoted string, ident, OR ident @ host
6442                    // (host may itself be quoted or bare).
6443                    match self.peek().clone() {
6444                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6445                            self.advance();
6446                            // Optional `@host`.
6447                            if matches!(self.peek(), Token::At) {
6448                                self.advance();
6449                                if matches!(
6450                                    self.peek(),
6451                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6452                                ) {
6453                                    self.advance();
6454                                }
6455                            }
6456                        }
6457                        _ => {}
6458                    }
6459                }
6460                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6461                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6462                    // when followed by SECURITY — the dispatcher must
6463                    // not consume a bare `SQL` token (it's not a
6464                    // legal CREATE prefix on its own).
6465                    let save = self.pos;
6466                    self.advance(); // SQL
6467                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6468                        if s2.eq_ignore_ascii_case("security"))
6469                    {
6470                        self.advance(); // SECURITY
6471                        // DEFINER / INVOKER trailing ident.
6472                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6473                            self.advance();
6474                        }
6475                    } else {
6476                        // Not a SQL SECURITY clause — roll back and
6477                        // bail; the caller will error out cleanly.
6478                        self.pos = save;
6479                        return Ok(());
6480                    }
6481                }
6482                _ => return Ok(()),
6483            }
6484        }
6485    }
6486
6487    fn parse_if_not_exists(&mut self) -> bool {
6488        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6489        {
6490            let save = self.pos;
6491            self.advance();
6492            if matches!(self.peek(), Token::Not) {
6493                self.advance();
6494                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6495                {
6496                    self.advance();
6497                    return true;
6498                }
6499            }
6500            self.pos = save;
6501        }
6502        false
6503    }
6504
6505    fn parse_if_exists(&mut self) -> bool {
6506        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6507        {
6508            let save = self.pos;
6509            self.advance();
6510            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6511            {
6512                self.advance();
6513                return true;
6514            }
6515            self.pos = save;
6516        }
6517        false
6518    }
6519
6520    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6521    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6522    /// been consumed.
6523    fn parse_create_trigger_after_keyword(
6524        &mut self,
6525        or_replace: bool,
6526    ) -> Result<Statement, ParseError> {
6527        let name = self.expect_ident_like()?;
6528        let timing = {
6529            let ident = self.expect_ident_like()?;
6530            if ident.eq_ignore_ascii_case("before") {
6531                TriggerTiming::Before
6532            } else if ident.eq_ignore_ascii_case("after") {
6533                TriggerTiming::After
6534            } else if ident.eq_ignore_ascii_case("instead") {
6535                let next = self.expect_ident_like()?;
6536                if !next.eq_ignore_ascii_case("of") {
6537                    return Err(self.err(alloc::format!(
6538                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6539                    )));
6540                }
6541                TriggerTiming::InsteadOf
6542            } else {
6543                return Err(self.err(alloc::format!(
6544                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6545                )));
6546            }
6547        };
6548        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6549        // OR is a reserved keyword token (Token::Or), not an Ident.
6550        // v7.13.0 — after an UPDATE event we may optionally see
6551        // `OF col, col, …` (mailrs round-5 G7). Columns are
6552        // captured into `update_columns` once across the whole
6553        // events list; multiple `UPDATE OF` clauses are rejected.
6554        let mut events: Vec<TriggerEvent> = Vec::new();
6555        let mut update_columns: Vec<String> = Vec::new();
6556        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6557        events.push(first_ev);
6558        if !first_cols.is_empty() {
6559            update_columns = first_cols;
6560        }
6561        while matches!(self.peek(), Token::Or) {
6562            self.advance();
6563            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6564            events.push(ev);
6565            if !cols.is_empty() {
6566                if !update_columns.is_empty() {
6567                    return Err(
6568                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6569                    );
6570                }
6571                update_columns = cols;
6572            }
6573        }
6574        // ON <table>
6575        let tok = self.peek();
6576        let Token::On = tok else {
6577            return Err(self.err(alloc::format!(
6578                "expected ON after trigger events, got {tok:?}"
6579            )));
6580        };
6581        self.advance();
6582        let table = self.expect_ident_like()?;
6583        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6584        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6585        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6586        // the trigger as a plain AFTER trigger (correct for every non-deferred
6587        // use; deferral timing is not yet honoured).
6588        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6589            if s.eq_ignore_ascii_case("from"))
6590        {
6591            self.advance();
6592            let _reftable = self.expect_ident_like()?;
6593        }
6594        self.consume_optional_deferrable_clauses()?;
6595        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6596        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6597        // idents.
6598        if !matches!(self.peek(), Token::For) {
6599            return Err(self.err(alloc::format!(
6600                "expected FOR EACH ROW / STATEMENT, got {:?}",
6601                self.peek()
6602            )));
6603        }
6604        self.advance();
6605        let for_each = {
6606            let e = self.expect_ident_like()?;
6607            if !e.eq_ignore_ascii_case("each") {
6608                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6609            }
6610            let unit = self.expect_ident_like()?;
6611            if unit.eq_ignore_ascii_case("row") {
6612                TriggerForEach::Row
6613            } else if unit.eq_ignore_ascii_case("statement") {
6614                TriggerForEach::Statement
6615            } else {
6616                return Err(self.err(alloc::format!(
6617                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6618                )));
6619            }
6620        };
6621        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6622        let when_condition = if matches!(self.peek(),
6623            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6624        {
6625            self.advance();
6626            Some(self.parse_paren_expr("WHEN")?)
6627        } else {
6628            None
6629        };
6630        // EXECUTE FUNCTION/PROCEDURE name(...)
6631        let exec = self.expect_ident_like()?;
6632        if !exec.eq_ignore_ascii_case("execute") {
6633            return Err(self.err(alloc::format!(
6634                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6635            )));
6636        }
6637        let fn_or_proc = self.expect_ident_like()?;
6638        if !(fn_or_proc.eq_ignore_ascii_case("function")
6639            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6640        {
6641            return Err(self.err(alloc::format!(
6642                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6643            )));
6644        }
6645        let function = self.expect_ident_like()?;
6646        // Optional empty arg list `()`.
6647        if matches!(self.peek(), Token::LParen) {
6648            self.advance();
6649            if !matches!(self.peek(), Token::RParen) {
6650                return Err(self.err(alloc::format!(
6651                    "v7.12.4 trigger function calls take no args; got {:?}",
6652                    self.peek()
6653                )));
6654            }
6655            self.advance();
6656        }
6657        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6658            name,
6659            or_replace,
6660            timing,
6661            events,
6662            table,
6663            for_each,
6664            function,
6665            update_columns,
6666            when_condition,
6667        }))
6668    }
6669
6670    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6671    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6672    fn parse_create_rule_after_keyword(
6673        &mut self,
6674        or_replace: bool,
6675    ) -> Result<Statement, ParseError> {
6676        let name = self.expect_ident_like()?;
6677        if !matches!(self.peek(), Token::As) {
6678            return Err(self.err(alloc::format!(
6679                "expected AS in CREATE RULE, got {:?}",
6680                self.peek()
6681            )));
6682        }
6683        self.advance();
6684        if !matches!(self.peek(), Token::On) {
6685            return Err(self.err(alloc::format!(
6686                "expected ON in CREATE RULE, got {:?}",
6687                self.peek()
6688            )));
6689        }
6690        self.advance();
6691        let event = self.parse_rule_event()?;
6692        if !matches!(self.peek(), Token::To)
6693            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6694        {
6695            return Err(self.err(alloc::format!(
6696                "expected TO after rule event, got {:?}",
6697                self.peek()
6698            )));
6699        }
6700        self.advance();
6701        let table = self.expect_ident_like()?;
6702        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6703        let when_condition = if matches!(self.peek(), Token::Where) {
6704            self.advance();
6705            Some(self.parse_expr(0)?)
6706        } else {
6707            None
6708        };
6709        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6710        {
6711            return Err(self.err(alloc::format!(
6712                "expected DO in CREATE RULE, got {:?}",
6713                self.peek()
6714            )));
6715        }
6716        self.advance();
6717        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6718        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6719        {
6720            self.advance();
6721            true
6722        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6723            self.advance();
6724            false
6725        } else {
6726            false
6727        };
6728        // `NOTHING` | `( cmd; … )` | `cmd`.
6729        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6730        {
6731            self.advance();
6732            Vec::new()
6733        } else if matches!(self.peek(), Token::LParen) {
6734            self.advance();
6735            let mut cmds = Vec::new();
6736            loop {
6737                cmds.push(self.parse_one_statement()?);
6738                if matches!(self.peek(), Token::Semicolon) {
6739                    self.advance();
6740                    if matches!(self.peek(), Token::RParen) {
6741                        break;
6742                    }
6743                    continue;
6744                }
6745                break;
6746            }
6747            if !matches!(self.peek(), Token::RParen) {
6748                return Err(self.err(alloc::format!(
6749                    "expected ) closing the CREATE RULE command list, got {:?}",
6750                    self.peek()
6751                )));
6752            }
6753            self.advance();
6754            cmds
6755        } else {
6756            alloc::vec![self.parse_one_statement()?]
6757        };
6758        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6759            name,
6760            or_replace,
6761            event,
6762            table,
6763            instead,
6764            when_condition,
6765            commands,
6766        }))
6767    }
6768
6769    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6770    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6771        if matches!(self.peek(), Token::Insert) {
6772            self.advance();
6773            return Ok(alloc::string::String::from("INSERT"));
6774        }
6775        if matches!(self.peek(), Token::Select) {
6776            self.advance();
6777            return Ok(alloc::string::String::from("SELECT"));
6778        }
6779        match self.peek() {
6780            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6781                self.advance();
6782                Ok(alloc::string::String::from("UPDATE"))
6783            }
6784            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6785                self.advance();
6786                Ok(alloc::string::String::from("DELETE"))
6787            }
6788            other => Err(self.err(alloc::format!(
6789                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6790            ))),
6791        }
6792    }
6793
6794    /// v7.13.0 — parse one trigger event, then optionally consume
6795    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6796    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6797    fn parse_trigger_event_with_optional_of(
6798        &mut self,
6799    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6800        let ev = self.parse_trigger_event()?;
6801        if !matches!(ev, TriggerEvent::Update) {
6802            return Ok((ev, Vec::new()));
6803        }
6804        // `OF` is a bare ident.
6805        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6806            return Ok((ev, Vec::new()));
6807        }
6808        self.advance(); // OF
6809        let mut cols: Vec<String> = Vec::new();
6810        loop {
6811            cols.push(self.expect_ident_like()?);
6812            if matches!(self.peek(), Token::Comma) {
6813                self.advance();
6814                continue;
6815            }
6816            break;
6817        }
6818        if cols.is_empty() {
6819            return Err(
6820                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6821            );
6822        }
6823        Ok((ev, cols))
6824    }
6825
6826    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6827    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6828    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6829    /// inside the body.
6830    /// Called by [`parse_plpgsql_body`] after the body's tokens
6831    /// have been lexed into this temporary parser.
6832    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6833        // v7.12.6 — optional DECLARE prelude.
6834        let declarations = if matches!(
6835            self.peek(),
6836            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6837        ) {
6838            self.advance();
6839            self.parse_plpgsql_declare_block()?
6840        } else {
6841            Vec::new()
6842        };
6843        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6844        // `BEGIN` transaction-start, but we can reuse the
6845        // reserved Token::Begin since the body is a separate
6846        // lex/parse context).
6847        if !matches!(self.peek(), Token::Begin) {
6848            return Err(self.err(alloc::format!(
6849                "expected BEGIN at start of plpgsql block, got {:?}",
6850                self.peek()
6851            )));
6852        }
6853        self.advance();
6854        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6855        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6856        // body's last statement and the trailing END. When present
6857        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6858        // arms terminated by END.
6859        let exception_handlers = if matches!(
6860            self.peek(),
6861            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6862        ) {
6863            self.advance();
6864            self.parse_plpgsql_exception_handlers()?
6865        } else {
6866            Vec::new()
6867        };
6868        Ok(PlPgSqlBlock {
6869            declarations,
6870            statements,
6871            exception_handlers,
6872        })
6873    }
6874
6875    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6876    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6877    fn parse_plpgsql_exception_handlers(
6878        &mut self,
6879    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6880        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6881        loop {
6882            // Stop at END — the block-level trailing END LOOP / END;
6883            // is handled by the caller.
6884            if matches!(
6885                self.peek(),
6886                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6887            ) {
6888                return Ok(out);
6889            }
6890            // WHEN <cond> [OR <cond>]* THEN <body>
6891            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6892            {
6893                return Err(self.err(alloc::format!(
6894                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
6895                    self.peek()
6896                )));
6897            }
6898            self.advance();
6899            let mut conditions: Vec<String> = Vec::new();
6900            conditions.push(self.expect_ident_like()?);
6901            while matches!(self.peek(), Token::Or) {
6902                self.advance();
6903                conditions.push(self.expect_ident_like()?);
6904            }
6905            let then_kw = self.expect_ident_like()?;
6906            if !then_kw.eq_ignore_ascii_case("then") {
6907                return Err(self.err(alloc::format!(
6908                    "expected THEN after WHEN condition list, got {then_kw:?}"
6909                )));
6910            }
6911            let body = self.parse_plpgsql_stmt_list_until_end()?;
6912            out.push(crate::ast::ExceptionHandler { conditions, body });
6913        }
6914    }
6915
6916    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6917    /// prelude. Caller has already consumed `DECLARE`. We stop
6918    /// reading entries when we hit `BEGIN`.
6919    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6920        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6921        loop {
6922            if matches!(self.peek(), Token::Begin) {
6923                return Ok(out);
6924            }
6925            let name = self.expect_ident_like()?;
6926            // v7.37.20 (20.7) — type inference: if the next token is
6927            // `:=` or `=` (no explicit type), infer from the default
6928            // expression. Otherwise the ident that follows is the
6929            // declared type.
6930            //
6931            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6932            // (PG-standard). SPG parse-accepts and treats identically
6933            // to inference — the eventual runtime value determines
6934            // the local's type, which is faithful to how SPG handles
6935            // untyped locals today (see 20.7). Full compile-time
6936            // catalog lookup queues with v7.40 PL/pgSQL epic.
6937            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6938                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6939                // downstream declaration walker to type the local by
6940                // the runtime type of the default expression.
6941                FunctionArgType::Raw("_infer_".into())
6942            } else {
6943                let ty_token = self.expect_ident_like()?;
6944                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6945                // consume optional `.<ident>` qualifier + `%<KW>`
6946                // suffix. Both qualifier and suffix map to _infer_.
6947                if matches!(self.peek(), Token::Dot) {
6948                    self.advance();
6949                    let _ = self.expect_ident_like()?;
6950                }
6951                if matches!(self.peek(), Token::Percent) {
6952                    self.advance();
6953                    // Consume the trailing TYPE / ROWTYPE ident.
6954                    let _ = self.expect_ident_like()?;
6955                    FunctionArgType::Raw("_infer_".into())
6956                } else {
6957                    match map_type_ident_to_column_type_name(&ty_token) {
6958                        Some(t) => FunctionArgType::Typed(t),
6959                        None => FunctionArgType::Raw(ty_token),
6960                    }
6961                }
6962            };
6963            let default = match self.peek() {
6964                Token::ColonEq => {
6965                    self.advance();
6966                    Some(self.parse_expr(0)?)
6967                }
6968                Token::Eq => {
6969                    // PL/pgSQL also accepts `=` for the
6970                    // DECLARE default (PG treats them the same
6971                    // in this position).
6972                    self.advance();
6973                    Some(self.parse_expr(0)?)
6974                }
6975                _ => None,
6976            };
6977            // Mandatory `;` between declarations.
6978            if !matches!(self.peek(), Token::Semicolon) {
6979                return Err(self.err(alloc::format!(
6980                    "expected ; after DECLARE entry for {name:?}, got {:?}",
6981                    self.peek()
6982                )));
6983            }
6984            self.advance();
6985            out.push(PlPgSqlDeclare { name, ty, default });
6986        }
6987    }
6988
6989    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6990    /// the terminating `END;` (or `END IF;` etc — handled by the
6991    /// per-construct sub-parsers). Used by both the outer block
6992    /// and the IF/ELSE branch bodies.
6993    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6994        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6995        loop {
6996            // Allow trailing semicolons + END.
6997            while matches!(self.peek(), Token::Semicolon) {
6998                self.advance();
6999            }
7000            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7001            if matches!(
7002                self.peek(),
7003                Token::Ident(s) | Token::QuotedIdent(s)
7004                    if s.eq_ignore_ascii_case("end")
7005                        || s.eq_ignore_ascii_case("else")
7006                        || s.eq_ignore_ascii_case("elsif")
7007                        || s.eq_ignore_ascii_case("elseif")
7008                        || s.eq_ignore_ascii_case("exception")
7009                        || s.eq_ignore_ascii_case("when")
7010            ) {
7011                return Ok(statements);
7012            }
7013            // Otherwise: one statement, then expect `;` or
7014            // a block-terminator keyword.
7015            let stmt = self.parse_plpgsql_stmt()?;
7016            statements.push(stmt);
7017            match self.peek() {
7018                Token::Semicolon => {
7019                    self.advance();
7020                }
7021                Token::Ident(s) | Token::QuotedIdent(s)
7022                    if s.eq_ignore_ascii_case("end")
7023                        || s.eq_ignore_ascii_case("else")
7024                        || s.eq_ignore_ascii_case("elsif")
7025                        || s.eq_ignore_ascii_case("elseif")
7026                        || s.eq_ignore_ascii_case("exception")
7027                        || s.eq_ignore_ascii_case("when") =>
7028                {
7029                    // Final statement of the block without `;`.
7030                }
7031                other => {
7032                    return Err(self.err(alloc::format!(
7033                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7034                    )));
7035                }
7036            }
7037        }
7038    }
7039
7040    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7041        // RETURN keyword?
7042        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7043        {
7044            self.advance();
7045            return self.parse_plpgsql_return();
7046        }
7047        // v7.12.6 — IF block.
7048        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7049        {
7050            self.advance();
7051            return self.parse_plpgsql_if();
7052        }
7053        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7054        // Detected by peeking that token pos+3 is Ident("execute").
7055        if matches!(self.peek(), Token::For)
7056            && matches!(
7057                self.tokens.get(self.pos + 1),
7058                Some(Token::Ident(_) | Token::QuotedIdent(_))
7059            )
7060            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7061            && matches!(
7062                self.tokens.get(self.pos + 3),
7063                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7064            )
7065        {
7066            self.advance(); // FOR
7067            let var = self.expect_ident_like()?;
7068            self.advance(); // IN
7069            self.advance(); // EXECUTE
7070            // Prescan for LOOP at paren depth 0 so parse_expr stops
7071            // before the LOOP keyword (same trick as the bare-SELECT
7072            // ForQuery arm).
7073            let mut depth: i32 = 0;
7074            let mut loop_pos: Option<usize> = None;
7075            let mut scan = self.pos;
7076            while scan < self.tokens.len() {
7077                match self.tokens.get(scan) {
7078                    Some(Token::LParen) => depth += 1,
7079                    Some(Token::RParen) => depth -= 1,
7080                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7081                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7082                    {
7083                        loop_pos = Some(scan);
7084                        break;
7085                    }
7086                    _ => {}
7087                }
7088                scan += 1;
7089            }
7090            let loop_pos = loop_pos.ok_or_else(|| {
7091                self.err(alloc::format!(
7092                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7093                ))
7094            })?;
7095            let saved_loop = self.tokens[loop_pos].clone();
7096            self.tokens[loop_pos] = Token::Semicolon;
7097            let expr_result = self.parse_expr(0);
7098            self.tokens[loop_pos] = saved_loop;
7099            let sql_expr = expr_result?;
7100            let loop_kw = self.expect_ident_like()?;
7101            if !loop_kw.eq_ignore_ascii_case("loop") {
7102                return Err(self.err(alloc::format!(
7103                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7104                )));
7105            }
7106            let body = self.parse_plpgsql_stmt_list_until_end()?;
7107            let end_kw = self.expect_ident_like()?;
7108            if !end_kw.eq_ignore_ascii_case("end") {
7109                return Err(self.err(alloc::format!(
7110                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7111                )));
7112            }
7113            let loop_kw2 = self.expect_ident_like()?;
7114            if !loop_kw2.eq_ignore_ascii_case("loop") {
7115                return Err(self.err(alloc::format!(
7116                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7117                )));
7118            }
7119            return Ok(PlPgSqlStmt::ForExecute {
7120                var,
7121                sql_expr,
7122                body,
7123            });
7124        }
7125        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7126        //
7127        // Two syntactic forms:
7128        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7129        //   FOR var IN (SELECT ...) LOOP ...
7130        //
7131        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7132        // the trailing `LOOP` keyword as a table alias, we prescan
7133        // forward to find LOOP at paren depth 0, splice a fake
7134        // Semicolon at that position (so SELECT parses cleanly),
7135        // then re-splice LOOP back in.
7136        //
7137        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7138        // LOOP directly — no scan required.
7139        if matches!(self.peek(), Token::For)
7140            && matches!(
7141                self.tokens.get(self.pos + 1),
7142                Some(Token::Ident(_) | Token::QuotedIdent(_))
7143            )
7144            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7145            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7146                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7147        {
7148            self.advance(); // FOR
7149            let var = self.expect_ident_like()?;
7150            // IN
7151            self.advance();
7152            let query = if matches!(self.peek(), Token::LParen) {
7153                // Paren-wrapped SELECT.
7154                self.advance();
7155                let inner = self.parse_select_stmt()?;
7156                let Statement::Select(q) = inner else {
7157                    return Err(self.err(alloc::format!(
7158                        "expected SELECT inside (…), got {:?}",
7159                        self.peek()
7160                    )));
7161                };
7162                if !matches!(self.peek(), Token::RParen) {
7163                    return Err(self.err(alloc::format!(
7164                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7165                        self.peek()
7166                    )));
7167                }
7168                self.advance();
7169                q
7170            } else {
7171                // Bare SELECT: prescan to find the LOOP boundary.
7172                let mut depth: i32 = 0;
7173                let mut loop_pos: Option<usize> = None;
7174                let mut scan = self.pos;
7175                while scan < self.tokens.len() {
7176                    match self.tokens.get(scan) {
7177                        Some(Token::LParen) => depth += 1,
7178                        Some(Token::RParen) => depth -= 1,
7179                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7180                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7181                        {
7182                            loop_pos = Some(scan);
7183                            break;
7184                        }
7185                        _ => {}
7186                    }
7187                    scan += 1;
7188                }
7189                let loop_pos = loop_pos.ok_or_else(|| {
7190                    self.err(alloc::format!(
7191                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7192                    ))
7193                })?;
7194                // Swap the LOOP token with a synthetic Semicolon so
7195                // parse_select_stmt stops there, then restore afterward.
7196                let saved_loop = self.tokens[loop_pos].clone();
7197                self.tokens[loop_pos] = Token::Semicolon;
7198                let parse_result = self.parse_select_stmt();
7199                self.tokens[loop_pos] = saved_loop;
7200                let inner = parse_result?;
7201                let Statement::Select(q) = inner else {
7202                    return Err(self.err(alloc::format!(
7203                        "expected SELECT after FOR <var> IN, got {:?}",
7204                        self.peek()
7205                    )));
7206                };
7207                q
7208            };
7209            let loop_kw = self.expect_ident_like()?;
7210            if !loop_kw.eq_ignore_ascii_case("loop") {
7211                return Err(self.err(alloc::format!(
7212                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7213                )));
7214            }
7215            let body = self.parse_plpgsql_stmt_list_until_end()?;
7216            let end_kw = self.expect_ident_like()?;
7217            if !end_kw.eq_ignore_ascii_case("end") {
7218                return Err(self.err(alloc::format!(
7219                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7220                )));
7221            }
7222            let loop_kw2 = self.expect_ident_like()?;
7223            if !loop_kw2.eq_ignore_ascii_case("loop") {
7224                return Err(self.err(alloc::format!(
7225                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7226                )));
7227            }
7228            return Ok(PlPgSqlStmt::ForQuery {
7229                var,
7230                query: Box::new(query),
7231                body,
7232            });
7233        }
7234        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7235        // FOR is a reserved keyword token (Token::For).
7236        if matches!(self.peek(), Token::For)
7237            && matches!(
7238                self.tokens.get(self.pos + 1),
7239                Some(Token::Ident(_) | Token::QuotedIdent(_))
7240            )
7241            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7242        {
7243            self.advance(); // FOR
7244            let var = self.expect_ident_like()?;
7245            if !matches!(self.peek(), Token::In) {
7246                return Err(self.err(alloc::format!(
7247                    "expected IN after FOR <var>, got {:?}",
7248                    self.peek()
7249                )));
7250            }
7251            self.advance();
7252            let reverse = matches!(
7253                self.peek(),
7254                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7255            );
7256            if reverse {
7257                self.advance();
7258            }
7259            let start = self.parse_expr(0)?;
7260            if !matches!(self.peek(), Token::DotDot) {
7261                return Err(self.err(alloc::format!(
7262                    "expected '..' between FOR loop bounds, got {:?}",
7263                    self.peek()
7264                )));
7265            }
7266            self.advance();
7267            let end = self.parse_expr(0)?;
7268            let loop_kw = self.expect_ident_like()?;
7269            if !loop_kw.eq_ignore_ascii_case("loop") {
7270                return Err(self.err(alloc::format!(
7271                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7272                )));
7273            }
7274            let body = self.parse_plpgsql_stmt_list_until_end()?;
7275            let end_kw = self.expect_ident_like()?;
7276            if !end_kw.eq_ignore_ascii_case("end") {
7277                return Err(self.err(alloc::format!(
7278                    "expected END LOOP after FOR body, got {end_kw:?}"
7279                )));
7280            }
7281            let loop_kw2 = self.expect_ident_like()?;
7282            if !loop_kw2.eq_ignore_ascii_case("loop") {
7283                return Err(self.err(alloc::format!(
7284                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7285                )));
7286            }
7287            return Ok(PlPgSqlStmt::ForRange {
7288                var,
7289                start,
7290                end,
7291                reverse,
7292                body,
7293            });
7294        }
7295        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7296        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7297        {
7298            self.advance();
7299            let body = self.parse_plpgsql_stmt_list_until_end()?;
7300            let end_kw = self.expect_ident_like()?;
7301            if !end_kw.eq_ignore_ascii_case("end") {
7302                return Err(self.err(alloc::format!(
7303                    "expected END LOOP after LOOP body, got {end_kw:?}"
7304                )));
7305            }
7306            let loop_kw = self.expect_ident_like()?;
7307            if !loop_kw.eq_ignore_ascii_case("loop") {
7308                return Err(self.err(alloc::format!(
7309                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7310                )));
7311            }
7312            return Ok(PlPgSqlStmt::Loop { body });
7313        }
7314        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7315        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7316        {
7317            self.advance();
7318            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7319            {
7320                self.advance();
7321                Some(self.parse_expr(0)?)
7322            } else {
7323                None
7324            };
7325            return Ok(PlPgSqlStmt::Exit { when });
7326        }
7327        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7328        // already-parsed Statement or a runtime-computed SQL string.
7329        // The disambiguator vs the extended-query-protocol `EXECUTE
7330        // <stmt_name>` (which is a top-level Statement, not a
7331        // plpgsql line) is that inside a DO block / trigger body the
7332        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7333        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7334        {
7335            self.advance();
7336            let sql = self.parse_expr(0)?;
7337            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7338        }
7339        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7340        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7341        {
7342            self.advance();
7343            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7344            {
7345                self.advance();
7346                Some(self.parse_expr(0)?)
7347            } else {
7348                None
7349            };
7350            return Ok(PlPgSqlStmt::Continue { when });
7351        }
7352        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7353        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7354        {
7355            self.advance();
7356            let condition = self.parse_expr(0)?;
7357            let loop_kw = self.expect_ident_like()?;
7358            if !loop_kw.eq_ignore_ascii_case("loop") {
7359                return Err(self.err(alloc::format!(
7360                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7361                )));
7362            }
7363            let body = self.parse_plpgsql_stmt_list_until_end()?;
7364            // Expect END LOOP.
7365            let end_kw = self.expect_ident_like()?;
7366            if !end_kw.eq_ignore_ascii_case("end") {
7367                return Err(self.err(alloc::format!(
7368                    "expected END LOOP after WHILE body, got {end_kw:?}"
7369                )));
7370            }
7371            let loop_kw2 = self.expect_ident_like()?;
7372            if !loop_kw2.eq_ignore_ascii_case("loop") {
7373                return Err(self.err(alloc::format!(
7374                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7375                )));
7376            }
7377            return Ok(PlPgSqlStmt::While { condition, body });
7378        }
7379        // v7.12.6 — RAISE.
7380        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7381        {
7382            self.advance();
7383            return self.parse_plpgsql_raise();
7384        }
7385        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7386        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7387        {
7388            self.advance();
7389            let condition = self.parse_expr(0)?;
7390            let message = if matches!(self.peek(), Token::Comma) {
7391                self.advance();
7392                Some(self.parse_expr(0)?)
7393            } else {
7394                None
7395            };
7396            return Ok(PlPgSqlStmt::Assert { condition, message });
7397        }
7398        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7399        //   "PERFORM is equivalent to SELECT but discards the
7400        //    result." Side effects (function calls, RAISE inside
7401        //    SQL functions, etc.) still execute. We desugar to
7402        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7403        //    existing embedded-statement path handles execution +
7404        //    result-discard cleanly. The result is naturally
7405        //    discarded because EmbeddedSql doesn't propagate row
7406        //    sets back to the plpgsql interpreter.
7407        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7408        {
7409            self.advance();
7410            // Splice a synthetic Token::Select into the stream at
7411            // the current position so parse_select_stmt parses the
7412            // remainder as a normal SELECT body. Token-stream
7413            // surgery mirrors the try_parse_plpgsql_select_into
7414            // pattern used for SELECT … INTO desugaring.
7415            self.tokens.insert(self.pos, Token::Select);
7416            let select = self.parse_select_stmt()?;
7417            let Statement::Select(s) = select else {
7418                return Err(self.err(alloc::format!(
7419                    "expected SELECT body after PERFORM, got {:?}",
7420                    self.peek()
7421                )));
7422            };
7423            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7424        }
7425        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7426        // plpgsql-specific shape (mailrs round-10 migrate-042).
7427        // PG's SELECT INTO at top-level SQL would CREATE a new
7428        // table; inside plpgsql it ASSIGNS the query result to
7429        // a local variable. We detect the INTO at paren-depth
7430        // 0 between SELECT and the statement boundary; if
7431        // found, split the token stream into "pre-INTO
7432        // projection" + "var" + "post-INTO FROM/WHERE…" and
7433        // rebuild as a SelectInto with a regular SELECT body
7434        // (no INTO clause).
7435        if matches!(self.peek(), Token::Select)
7436            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7437        {
7438            return Ok(PlPgSqlStmt::SelectInto {
7439                var: var_name,
7440                body: Box::new(select_body),
7441            });
7442        }
7443        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7444        // SELECT can appear directly inside a trigger body; we
7445        // recurse into the regular Statement parser, which will
7446        // stop at the trailing `;` (which our caller then
7447        // consumes).
7448        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7449        // also embed ALTER / CREATE / DROP statements; route
7450        // those through the same parser so the DO body parses
7451        // cleanly.
7452        if matches!(self.peek(), Token::Insert)
7453            || matches!(self.peek(), Token::Select)
7454            || matches!(self.peek(), Token::Create)
7455            || matches!(self.peek(), Token::Drop)
7456            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7457                if s.eq_ignore_ascii_case("update")
7458                    || s.eq_ignore_ascii_case("delete")
7459                    || s.eq_ignore_ascii_case("alter"))
7460        {
7461            let stmt = self.parse_one_statement()?;
7462            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7463        }
7464        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7465        // followed by `:=` and an expression.
7466        let target = self.parse_plpgsql_assign_target()?;
7467        // PL/pgSQL assignment uses `:=`. The lexer represents
7468        // this as a colon followed by `=`; check both shapes.
7469        match self.peek() {
7470            Token::ColonEq => {
7471                self.advance();
7472            }
7473            Token::Colon => {
7474                self.advance();
7475                if !matches!(self.peek(), Token::Eq) {
7476                    return Err(self.err(alloc::format!(
7477                        "expected := after plpgsql assign target, got `:` then {:?}",
7478                        self.peek()
7479                    )));
7480                }
7481                self.advance();
7482            }
7483            other => {
7484                return Err(self.err(alloc::format!(
7485                    "expected := after plpgsql assign target, got {other:?}"
7486                )));
7487            }
7488        }
7489        let value = self.parse_expr(0)?;
7490        Ok(PlPgSqlStmt::Assign { target, value })
7491    }
7492
7493    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7494    /// [ELSE body] END IF`. `IF` keyword already consumed.
7495    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7496        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7497        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7498        loop {
7499            // <expr> THEN
7500            let cond = self.parse_expr(0)?;
7501            let then_kw = self.expect_ident_like()?;
7502            if !then_kw.eq_ignore_ascii_case("then") {
7503                return Err(self.err(alloc::format!(
7504                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7505                )));
7506            }
7507            let body = self.parse_plpgsql_stmt_list_until_end()?;
7508            branches.push((cond, body));
7509            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7510            match self.peek() {
7511                Token::Ident(s) | Token::QuotedIdent(s)
7512                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7513                {
7514                    self.advance();
7515                    continue;
7516                }
7517                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7518                    self.advance();
7519                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7520                    break;
7521                }
7522                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7523                    break;
7524                }
7525                other => {
7526                    return Err(self.err(alloc::format!(
7527                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7528                    )));
7529                }
7530            }
7531        }
7532        // Expect `END IF` (the END keyword is the one we're
7533        // looking at right now).
7534        let end_kw = self.expect_ident_like()?;
7535        if !end_kw.eq_ignore_ascii_case("end") {
7536            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7537        }
7538        let if_kw = self.expect_ident_like()?;
7539        if !if_kw.eq_ignore_ascii_case("if") {
7540            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7541        }
7542        Ok(PlPgSqlStmt::If {
7543            branches,
7544            else_branch,
7545        })
7546    }
7547
7548    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7549    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7550    /// is already consumed.
7551    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7552        let lvl_ident = self.expect_ident_like()?;
7553        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7554            "notice" => RaiseLevel::Notice,
7555            "warning" => RaiseLevel::Warning,
7556            "info" => RaiseLevel::Info,
7557            "log" => RaiseLevel::Log,
7558            "debug" => RaiseLevel::Debug,
7559            "exception" => RaiseLevel::Exception,
7560            other => {
7561                return Err(self.err(alloc::format!(
7562                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7563                )));
7564            }
7565        };
7566        // Message: required for v7.12.6. PG accepts a bare
7567        // RAISE-rethrow form (no message), reserved for future
7568        // RAISE-no-args support.
7569        let Token::String(msg) = self.peek() else {
7570            return Err(self.err(alloc::format!(
7571                "expected RAISE message string, got {:?}",
7572                self.peek()
7573            )));
7574        };
7575        let message = msg.clone();
7576        self.advance();
7577        // Optional comma-separated args (PG `%` format substitution).
7578        let mut args: Vec<Expr> = Vec::new();
7579        while matches!(self.peek(), Token::Comma) {
7580            self.advance();
7581            args.push(self.parse_expr(0)?);
7582        }
7583        Ok(PlPgSqlStmt::Raise {
7584            level,
7585            message,
7586            args,
7587        })
7588    }
7589
7590    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7591    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7592    /// migrate-042). Returns `(rebuilt_select_without_into,
7593    /// var_name)` when the pattern matches; `None` for
7594    /// regular SELECTs (those go through the embedded-SQL
7595    /// path). Token-stream surgery so the rebuilt SELECT
7596    /// parses through the regular `parse_select_stmt`.
7597    #[allow(clippy::too_many_lines)]
7598    fn try_parse_plpgsql_select_into(
7599        &mut self,
7600    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7601        // Scan forward from `self.pos + 1` (past Token::Select)
7602        // for Token::Into at paren-depth 0, stopping at the
7603        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7604        // end the plpgsql statement.
7605        let start = self.pos;
7606        let mut into_pos: Option<usize> = None;
7607        let mut depth: i32 = 0;
7608        let mut i = start + 1;
7609        while i < self.tokens.len() {
7610            match &self.tokens[i] {
7611                Token::LParen => depth += 1,
7612                Token::RParen => depth -= 1,
7613                Token::Semicolon if depth == 0 => break,
7614                Token::Ident(s)
7615                    if depth == 0
7616                        && (s.eq_ignore_ascii_case("end")
7617                            || s.eq_ignore_ascii_case("else")
7618                            || s.eq_ignore_ascii_case("elsif")) =>
7619                {
7620                    break;
7621                }
7622                Token::Into if depth == 0 => {
7623                    into_pos = Some(i);
7624                    break;
7625                }
7626                _ => {}
7627            }
7628            i += 1;
7629        }
7630        let Some(into_at) = into_pos else {
7631            return Ok(None);
7632        };
7633        // The token immediately after INTO must be the target
7634        // var ident; anything else (e.g. INSERT INTO table)
7635        // ruled out by the depth-0 check above. Capture it.
7636        let var = match self.tokens.get(into_at + 1) {
7637            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7638            other => {
7639                return Err(self.err(alloc::format!(
7640                    "expected variable name after SELECT … INTO, got {other:?}"
7641                )));
7642            }
7643        };
7644        // Find the end of the plpgsql SELECT INTO statement —
7645        // same boundary rules as the depth-0 scan above.
7646        let mut end = into_at + 2;
7647        let mut depth2: i32 = 0;
7648        while end < self.tokens.len() {
7649            match &self.tokens[end] {
7650                Token::LParen => depth2 += 1,
7651                Token::RParen => depth2 -= 1,
7652                Token::Semicolon if depth2 == 0 => break,
7653                Token::Ident(s)
7654                    if depth2 == 0
7655                        && (s.eq_ignore_ascii_case("end")
7656                            || s.eq_ignore_ascii_case("else")
7657                            || s.eq_ignore_ascii_case("elsif")) =>
7658                {
7659                    break;
7660                }
7661                _ => {}
7662            }
7663            end += 1;
7664        }
7665        // Rebuild a token stream that represents the SELECT
7666        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7667        // post-var tokens up to statement end]. Run the
7668        // regular `parse_select_stmt` against it.
7669        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7670        for j in start..into_at {
7671            rebuilt.push(self.tokens[j].clone());
7672        }
7673        for j in (into_at + 2)..end {
7674            rebuilt.push(self.tokens[j].clone());
7675        }
7676        rebuilt.push(Token::Eof);
7677        let saved_pos = self.pos;
7678        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7679        self.pos = 0;
7680        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7681        if !matches!(self.peek(), Token::Select) {
7682            self.tokens = saved_tokens;
7683            self.pos = saved_pos;
7684            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7685        }
7686        let sel = self.parse_select_stmt();
7687        self.tokens = saved_tokens;
7688        self.pos = end;
7689        let sel = sel?;
7690        let Statement::Select(body) = sel else {
7691            return Err(self.err(alloc::format!(
7692                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7693            )));
7694        };
7695        Ok(Some((body, var)))
7696    }
7697
7698    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7699        // v7.16.1 — read the head token DIRECTLY rather than
7700        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7701        // strip (`public.t` → `t`) inside `expect_ident_like`
7702        // greedily consumes any `ident . ident` pair, which
7703        // silently turned every `NEW.col := …` /
7704        // `OLD.col := …` plpgsql assignment into a Local("col")
7705        // assignment — the head "new"/"old" was eaten as if it
7706        // were a schema name and the Dot was consumed too, so
7707        // this function's own `peek() == Token::Dot` check
7708        // below never fired. Every BEFORE trigger that rewrote
7709        // a NEW cell was a silent no-op for two major releases
7710        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7711        // gate failures were investigated as v7.16.1 backlog.
7712        let head = match self.advance() {
7713            Token::Ident(s) | Token::QuotedIdent(s) => s,
7714            other => {
7715                return Err(self.err(alloc::format!(
7716                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7717                )));
7718            }
7719        };
7720        if matches!(self.peek(), Token::Dot) {
7721            self.advance();
7722            let col = self.expect_ident_like()?;
7723            if head.eq_ignore_ascii_case("new") {
7724                return Ok(AssignTarget::NewColumn(col));
7725            }
7726            if head.eq_ignore_ascii_case("old") {
7727                return Ok(AssignTarget::OldColumn(col));
7728            }
7729            return Err(self.err(alloc::format!(
7730                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7731                 got {head:?}.<col>"
7732            )));
7733        }
7734        Ok(AssignTarget::Local(head))
7735    }
7736
7737    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7738        // RETURN NEW / OLD / NULL — bare-ident forms.
7739        match self.peek() {
7740            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7741                self.advance();
7742                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7743            }
7744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7745                self.advance();
7746                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7747            }
7748            Token::Null => {
7749                self.advance();
7750                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7751            }
7752            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7753            // per PL/pgSQL convention.
7754            Token::Semicolon => {
7755                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7756            }
7757            _ => {}
7758        }
7759        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7760        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7761        // caller-visible effect (blocks don't return sets), so we
7762        // desugar it identically to PERFORM: parse the SELECT (or
7763        // EXECUTE dynamic) as embedded SQL that runs for side
7764        // effects and discards the result. RETURN NEXT <expr>
7765        // (single-row accumulator) queues with v7.40 SETOF function
7766        // infrastructure.
7767        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7768        // and keep going.
7769        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7770        {
7771            self.advance();
7772            let e = self.parse_expr(0)?;
7773            return Ok(PlPgSqlStmt::ReturnNext(e));
7774        }
7775        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7776        {
7777            self.advance();
7778            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7779            // rows go to the set, like the static form. It used to desugar to a
7780            // bare ExecuteDynamic, whose result was DISCARDED.
7781            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7782            {
7783                self.advance();
7784                let sql = self.parse_expr(0)?;
7785                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7786            }
7787            // Bare RETURN QUERY <select>. If the current token is
7788            // not already SELECT (e.g., the user wrote `RETURN QUERY
7789            // <projection> FROM ...` in a shorthand — rare but PG
7790            // accepts a bare projection here), splice one in. Same
7791            // trick as PERFORM.
7792            if !matches!(self.peek(), Token::Select) {
7793                self.tokens.insert(self.pos, Token::Select);
7794            }
7795            let select = self.parse_select_stmt()?;
7796            let Statement::Select(s) = select else {
7797                return Err(self.err(alloc::format!(
7798                    "expected SELECT body after RETURN QUERY, got {:?}",
7799                    self.peek()
7800                )));
7801            };
7802            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7803            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7804            // in a SETOF function is the entire answer thrown away.
7805            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7806        }
7807        // Fall through: parse a full expression.
7808        let e = self.parse_expr(0)?;
7809        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7810    }
7811
7812    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7813        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7814        // are ident-shaped (the parser keys off case-insensitive
7815        // match — same shape used by the top-level Update / Delete
7816        // dispatchers at parse_one_statement).
7817        if matches!(self.peek(), Token::Insert) {
7818            self.advance();
7819            return Ok(TriggerEvent::Insert);
7820        }
7821        match self.peek() {
7822            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7823                self.advance();
7824                Ok(TriggerEvent::Update)
7825            }
7826            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7827                self.advance();
7828                Ok(TriggerEvent::Delete)
7829            }
7830            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7831                self.advance();
7832                Ok(TriggerEvent::Truncate)
7833            }
7834            other => Err(self.err(alloc::format!(
7835                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7836            ))),
7837        }
7838    }
7839
7840    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7841    ///   - (no clause) → implicit `FOR ALL TABLES`
7842    ///   - `FOR ALL TABLES`
7843    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7844    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7845    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7846    ///     REJECTS the bare plural (`invalid publication object list`,
7847    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7848    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7849    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7850        let name = self.expect_ident_or_string()?;
7851        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7852        // shape so existing publications keep parsing identically.
7853        let scope = if matches!(self.peek(), Token::For) {
7854            self.advance();
7855            if matches!(self.peek(), Token::All) {
7856                self.advance();
7857                if !matches!(self.peek(), Token::Tables) {
7858                    return Err(self.err(format!(
7859                        "expected TABLES after FOR ALL, got {:?}",
7860                        self.peek()
7861                    )));
7862                }
7863                self.advance();
7864                if matches!(self.peek(), Token::Except) {
7865                    self.advance();
7866                    let tables = self.parse_publication_table_list()?;
7867                    PublicationScope::AllTablesExcept(tables)
7868                } else {
7869                    PublicationScope::AllTables
7870                }
7871            } else if matches!(self.peek(), Token::Table) {
7872                self.advance();
7873                let tables = self.parse_publication_table_list()?;
7874                PublicationScope::ForTables(tables)
7875            } else if matches!(self.peek(), Token::Tables) {
7876                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7877                // plural (`FOR TABLES t`) is REJECTED (`invalid
7878                // publication object list`); TABLES only pairs with
7879                // `IN SCHEMA`. The old arm accepted it on an
7880                // unverifiable "PG 19 accepts both" claim.
7881                self.advance();
7882                if !matches!(self.peek(), Token::In) {
7883                    return Err(self.err(alloc::string::String::from(
7884                        "invalid publication object list",
7885                    )));
7886                }
7887                self.advance();
7888                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7889                    return Err(self.err(format!(
7890                        "expected SCHEMA after FOR TABLES IN, got {:?}",
7891                        self.peek()
7892                    )));
7893                }
7894                self.advance();
7895                let schema = self.expect_ident_or_string()?;
7896                PublicationScope::TablesInSchema(schema)
7897            } else {
7898                return Err(self.err(format!(
7899                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7900                    self.peek()
7901                )));
7902            }
7903        } else {
7904            PublicationScope::AllTables
7905        };
7906        Ok(Statement::CreatePublication(CreatePublicationStatement {
7907            name,
7908            scope,
7909        }))
7910    }
7911
7912    /// v6.1.3 — Comma-separated identifier list for the publication
7913    /// FOR-clause. Requires at least one entry; empty list is a
7914    /// parse error (PG behaviour). Quoted idents are accepted; the
7915    /// names round-trip through `Display` as `quote_ident(name)`.
7916    ///
7917    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7918    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7919    /// pg_dump output. SPG's publication state today is per-table
7920    /// only (matching the pre-PG-15 surface); the col list + WHERE
7921    /// are parsed so dumps load through and the table name reaches
7922    /// `PublicationScope::ForTables`, but the filter is not enforced
7923    /// at publish time. Re-open when a customer dogfood gate
7924    /// requires per-row-filter or column-subset publish semantics
7925    /// (which gates on persistent slot state landing first, 21.12).
7926    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7927        let first = self.parse_publication_table_entry()?;
7928        let mut out = alloc::vec![first];
7929        while matches!(self.peek(), Token::Comma) {
7930            self.advance();
7931            out.push(self.parse_publication_table_entry()?);
7932        }
7933        Ok(out)
7934    }
7935
7936    /// One table entry inside a FOR TABLE clause:
7937    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7938    /// Returns just the table name; the column list + WHERE predicate
7939    /// are consumed and discarded per the parse-accept-discard
7940    /// commitment above.
7941    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7942        let name = self.expect_ident_like()?;
7943        // Optional column list — `(col, col, …)`.
7944        if matches!(self.peek(), Token::LParen) {
7945            self.advance();
7946            // Empty parens are a PG error too; require ≥ 1 column.
7947            let _ = self.expect_ident_like()?;
7948            while matches!(self.peek(), Token::Comma) {
7949                self.advance();
7950                let _ = self.expect_ident_like()?;
7951            }
7952            if !matches!(self.peek(), Token::RParen) {
7953                return Err(self.err(alloc::format!(
7954                    "expected ')' to close publication column list, got {:?}",
7955                    self.peek()
7956                )));
7957            }
7958            self.advance();
7959        }
7960        // Optional row filter — `WHERE (predicate)`.
7961        if matches!(self.peek(), Token::Where) {
7962            self.advance();
7963            if !matches!(self.peek(), Token::LParen) {
7964                return Err(self.err(alloc::format!(
7965                    "expected '(' after WHERE in publication row filter, got {:?}",
7966                    self.peek()
7967                )));
7968            }
7969            self.advance();
7970            let _ = self.parse_expr(0)?;
7971            if !matches!(self.peek(), Token::RParen) {
7972                return Err(self.err(alloc::format!(
7973                    "expected ')' to close publication WHERE filter, got {:?}",
7974                    self.peek()
7975                )));
7976            }
7977            self.advance();
7978        }
7979        Ok(name)
7980    }
7981
7982    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7983    ///                 CONNECTION '<conn>'
7984    ///                 PUBLICATION <pub> [, <pub> ...]`.
7985    ///
7986    /// The clause order is fixed (CONNECTION first, then
7987    /// PUBLICATION) to match PG. No WITH-options accepted in
7988    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7989    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7990        let name = self.expect_ident_or_string()?;
7991        if !matches!(self.peek(), Token::Connection) {
7992            return Err(self.err(format!(
7993                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7994                self.peek()
7995            )));
7996        }
7997        self.advance();
7998        let conn_str = self.expect_string_literal()?;
7999        if !matches!(self.peek(), Token::Publication) {
8000            return Err(self.err(format!(
8001                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8002                self.peek()
8003            )));
8004        }
8005        self.advance();
8006        // Reuse the publication FOR-list parser shape: at least one
8007        // identifier, comma-separated.
8008        let first = self.expect_ident_like()?;
8009        let mut publications = alloc::vec![first];
8010        while matches!(self.peek(), Token::Comma) {
8011            self.advance();
8012            publications.push(self.expect_ident_like()?);
8013        }
8014        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8015            name,
8016            conn_str,
8017            publications,
8018        }))
8019    }
8020
8021    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8022    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8023    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8024    /// that fit `u64`.
8025    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8026    /// qualifier is a *namespace* the app owns (`app.user_id`,
8027    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8028    /// to discard. So parse the raw segments here instead of
8029    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8030    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8031    /// a single segment and round-trip unchanged.
8032    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8033        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8034        loop {
8035            let seg = match self.advance() {
8036                Token::Ident(s) | Token::QuotedIdent(s) => s,
8037                other if unreserved_keyword_text(&other).is_some() => {
8038                    unreserved_keyword_text(&other).unwrap()
8039                }
8040                other => {
8041                    return Err(ParseError {
8042                        message: format!("expected parameter name, got {other:?}"),
8043                        token_pos: self.consumed_pos(),
8044                    });
8045                }
8046            };
8047            parts.push(seg);
8048            if matches!(self.peek(), Token::Dot) {
8049                self.advance();
8050                continue;
8051            }
8052            break;
8053        }
8054        Ok(parts.join(".").to_ascii_lowercase())
8055    }
8056
8057    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8058        Self::parse_set_value_inner(self)
8059    }
8060
8061    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8062        match self.advance() {
8063            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8064            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8065                Ok(crate::ast::SetValue::Default)
8066            }
8067            Token::Ident(s) | Token::QuotedIdent(s) => {
8068                let mut accum = s;
8069                while matches!(self.peek(), Token::Dot) {
8070                    self.advance();
8071                    let next = self.expect_ident_like()?;
8072                    accum.push('.');
8073                    accum.push_str(&next);
8074                }
8075                Ok(crate::ast::SetValue::Ident(accum))
8076            }
8077            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8078            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8079            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8080            // spellings that lex as keyword tokens, not idents:
8081            // `SET standard_conforming_strings = on` is in every
8082            // pg_dump preamble (`off` already lexes as an ident).
8083            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8084            // DEFAULT lexes as its keyword token, so the ident arm above
8085            // never saw it and the everyday reset form was a syntax error.
8086            Token::Default => Ok(crate::ast::SetValue::Default),
8087            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8088            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8089            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8090            // v7.14.0 — MySQL session/user variable RHS
8091            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8092            // Wrap as Ident so the SET handler can record it; the
8093            // engine treats `@VAR` / `@@VAR` values as opaque
8094            // strings.
8095            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8096            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8097            // is the common MySQL preamble shape. Allow a `+` or
8098            // `-` prefix on negative numerics for parity with PG
8099            // (some param defaults are negative).
8100            Token::Minus => match self.advance() {
8101                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8102                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8103                other => Err(self.err(format!(
8104                    "expected numeric after `-` in SET value, got {other:?}"
8105                ))),
8106            },
8107            other => Err(self.err(format!(
8108                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8109            ))),
8110        }
8111    }
8112
8113    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8114    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8115    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8116    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8117    /// present). Modes are comma-separated per PG; SPG also
8118    /// accepts space-separated for tolerance. READ ONLY / WRITE
8119    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8120    /// surface but not behaviorally honoured today).
8121    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8122    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8123    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8124    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8125    /// session default rather than forcing READ COMMITTED.
8126    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8127        let mut level = IsolationLevel::default();
8128        let mut have_level = false;
8129        loop {
8130            // ISOLATION LEVEL …
8131            let saw_isolation =
8132                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8133            if saw_isolation {
8134                self.advance(); // ISOLATION
8135                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8136                    return Err(self.err(alloc::format!(
8137                        "expected LEVEL after ISOLATION, got {:?}",
8138                        self.peek()
8139                    )));
8140                }
8141                self.advance(); // LEVEL
8142                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8143                let w1 = self
8144                    .expect_ident_like()
8145                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8146                let lc = w1.to_ascii_lowercase();
8147                level = match lc.as_str() {
8148                    "serializable" => IsolationLevel::Serializable,
8149                    "repeatable" => {
8150                        // Expect READ
8151                        let w2 = self
8152                            .expect_ident_like()
8153                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8154                        if !w2.eq_ignore_ascii_case("read") {
8155                            return Err(self.err(alloc::format!(
8156                                "expected READ after REPEATABLE, got {w2:?}"
8157                            )));
8158                        }
8159                        IsolationLevel::RepeatableRead
8160                    }
8161                    "read" => {
8162                        let w2 = self
8163                            .expect_ident_like()
8164                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8165                        match w2.to_ascii_lowercase().as_str() {
8166                            "committed" => IsolationLevel::ReadCommitted,
8167                            "uncommitted" => IsolationLevel::ReadUncommitted,
8168                            other => {
8169                                return Err(self.err(alloc::format!(
8170                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8171                                )));
8172                            }
8173                        }
8174                    }
8175                    other => {
8176                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8177                    }
8178                };
8179                have_level = true;
8180            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8181                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8182                self.advance();
8183                match self.peek().clone() {
8184                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8185                        self.advance();
8186                    }
8187                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8188                        self.advance();
8189                    }
8190                    other => {
8191                        return Err(self.err(alloc::format!(
8192                            "expected ONLY or WRITE after READ, got {other:?}"
8193                        )));
8194                    }
8195                }
8196            } else if matches!(self.peek(), Token::Not) {
8197                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8198                self.advance();
8199                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8200                    return Err(self.err(alloc::format!(
8201                        "expected DEFERRABLE after NOT, got {:?}",
8202                        self.peek()
8203                    )));
8204                }
8205                self.advance();
8206            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8207            {
8208                self.advance();
8209            } else {
8210                break;
8211            }
8212            // Optional comma between modes.
8213            if matches!(self.peek(), Token::Comma) {
8214                self.advance();
8215            }
8216        }
8217        Ok(have_level.then_some(level))
8218    }
8219
8220    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8221        // FOR is a v6.1.2-reserved keyword (Token::For). The
8222        // other two are bare idents — they've never needed lexer
8223        // support and we keep it that way.
8224        if !matches!(self.peek(), Token::For) {
8225            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8226        }
8227        self.advance();
8228        self.expect_keyword_ident("wal")?;
8229        self.expect_keyword_ident("position")?;
8230        let pos = self.expect_u64_literal()?;
8231        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8232        {
8233            self.advance();
8234            self.expect_keyword_ident("timeout")?;
8235            Some(self.expect_u64_literal()?)
8236        } else {
8237            None
8238        };
8239        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8240    }
8241
8242    /// v6.1.7 helper — consume a `Token::Integer` and check it
8243    /// fits `u64`. WAL positions and millisecond timeouts are
8244    /// non-negative.
8245    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8246        match self.advance() {
8247            Token::Integer(n) if n >= 0 => Ok(n as u64),
8248            Token::Integer(n) => Err(ParseError {
8249                message: format!("expected non-negative integer, got {n}"),
8250                token_pos: self.consumed_pos(),
8251            }),
8252            other => Err(ParseError {
8253                message: format!("expected integer literal, got {other:?}"),
8254                token_pos: self.consumed_pos(),
8255            }),
8256        }
8257    }
8258
8259    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8260    /// ROLE '<role>' (defaults to readonly). All string slots accept
8261    /// either a quoted ident or a quoted string literal.
8262    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8263    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8264    ///
8265    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8266    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8267    /// wire role) still parses — it is a different axis from the PG attributes.
8268    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8269    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8270    /// or RESET, so the plain attribute forms keep their old path.
8271    fn peeks_db_role_setting(&self) -> bool {
8272        let mut i = self.pos + 1; // past the object's name
8273        let word = |p: usize| -> Option<String> {
8274            match self.tokens.get(p) {
8275                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8276                Some(Token::In) => Some(String::from("in")),
8277                _ => None,
8278            }
8279        };
8280        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8281            i += 3; // IN DATABASE <name>
8282        }
8283        matches!(word(i).as_deref(), Some("set" | "reset"))
8284    }
8285
8286    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8287        use crate::ast::SetDbRoleSettingStatement;
8288        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8289        // identifier, so the ordinary name reader refuses it. Same trap
8290        // as TABLE / INDEX / FULL / DEFAULT before it.
8291        let name = if matches!(self.peek(), Token::All) {
8292            self.advance();
8293            String::from("all")
8294        } else {
8295            self.expect_ident_or_string()?
8296        };
8297        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8298        let all = name.eq_ignore_ascii_case("all");
8299        let (mut database, mut role) = if is_database {
8300            (Some(name), None)
8301        } else if all {
8302            (None, None)
8303        } else {
8304            (None, Some(name))
8305        };
8306        if matches!(self.peek(), Token::In) {
8307            self.advance();
8308            self.advance(); // DATABASE
8309            database = Some(self.expect_ident_or_string()?);
8310        }
8311        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8312        self.advance(); // SET | RESET
8313        if resetting && matches!(self.peek(), Token::All) {
8314            self.advance();
8315            self.consume_until_statement_boundary();
8316            return Ok(Statement::SetDbRoleSetting(Box::new(
8317                SetDbRoleSettingStatement {
8318                    database,
8319                    role,
8320                    param: None,
8321                    value: None,
8322                },
8323            )));
8324        }
8325        let param = self.expect_ident_like()?;
8326        let value = if resetting {
8327            None
8328        } else {
8329            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8330            // KEYWORD, so the ident-only check missed it and consumed
8331            // the word itself as the value — the same trap as ALL, one
8332            // clause over.
8333            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8334                self.advance();
8335            }
8336            Some(self.take_guc_value())
8337        };
8338        self.consume_until_statement_boundary();
8339        Ok(Statement::SetDbRoleSetting(Box::new(
8340            SetDbRoleSettingStatement {
8341                database,
8342                role,
8343                param: Some(param),
8344                value,
8345            },
8346        )))
8347    }
8348
8349    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8350    /// a quoted literal loses its quotes, a bare word or number does not.
8351    fn take_guc_value(&mut self) -> String {
8352        match self.advance() {
8353            Token::String(s) => s,
8354            Token::Integer(n) => format!("{n}"),
8355            Token::Float(f) => format!("{f}"),
8356            Token::Ident(s) | Token::QuotedIdent(s) => s,
8357            other => format!("{other:?}"),
8358        }
8359    }
8360
8361    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8362        let name = self.expect_ident_or_string()?;
8363        if self.peek_keyword_ident("with") {
8364            self.advance();
8365        }
8366        let mut password = String::new();
8367        let mut role = String::new();
8368        let mut login: Option<bool> = None;
8369        let mut inherit: Option<bool> = None;
8370        let mut superuser: Option<bool> = None;
8371        // Not a `while let`: the pattern would borrow `self` across the
8372        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8373        #[allow(clippy::while_let_loop)]
8374        loop {
8375            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8376                break;
8377            };
8378            match w.to_ascii_lowercase().as_str() {
8379                "password" => {
8380                    self.advance();
8381                    password = self.expect_string_literal()?;
8382                }
8383                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8384                // is the same slot.
8385                "encrypted" => {
8386                    self.advance();
8387                    self.expect_keyword_ident("password")?;
8388                    password = self.expect_string_literal()?;
8389                }
8390                "login" => {
8391                    self.advance();
8392                    login = Some(true);
8393                }
8394                "nologin" => {
8395                    self.advance();
8396                    login = Some(false);
8397                }
8398                "inherit" => {
8399                    self.advance();
8400                    inherit = Some(true);
8401                }
8402                "noinherit" => {
8403                    self.advance();
8404                    inherit = Some(false);
8405                }
8406                "superuser" => {
8407                    self.advance();
8408                    superuser = Some(true);
8409                }
8410                "nosuperuser" => {
8411                    self.advance();
8412                    superuser = Some(false);
8413                }
8414                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8415                "role" => {
8416                    self.advance();
8417                    role = self.expect_string_literal()?;
8418                }
8419                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8420                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8421                // accepted and ignored so a pg_dump role block restores. They
8422                // gate capabilities SPG does not have.
8423                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8424                | "noreplication" | "bypassrls" | "nobypassrls" => {
8425                    self.advance();
8426                }
8427                "connection" => {
8428                    self.advance();
8429                    self.expect_keyword_ident("limit")?;
8430                    self.advance(); // the number
8431                }
8432                "valid" => {
8433                    self.advance();
8434                    self.expect_keyword_ident("until")?;
8435                    self.expect_string_literal()?;
8436                }
8437                _ => break,
8438            }
8439        }
8440        if role.is_empty() {
8441            role = "readonly".to_string();
8442        }
8443        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8444            name,
8445            password,
8446            role,
8447            login,
8448            inherit,
8449            superuser,
8450            is_user,
8451        }))
8452    }
8453
8454    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8455    /// consumed the USING / WITH CHECK keyword.
8456    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8457        if !matches!(self.peek(), Token::LParen) {
8458            return Err(self.err(alloc::format!(
8459                "expected '(' after {clause}, got {:?}",
8460                self.peek()
8461            )));
8462        }
8463        self.advance();
8464        let e = self.parse_expr(0)?;
8465        if !matches!(self.peek(), Token::RParen) {
8466            return Err(self.err(alloc::format!(
8467                "expected ')' to close {clause}, got {:?}",
8468                self.peek()
8469            )));
8470        }
8471        self.advance();
8472        Ok(e)
8473    }
8474
8475    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8476    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8477        let mut roles = Vec::new();
8478        loop {
8479            roles.push(self.expect_ident_like()?);
8480            if matches!(self.peek(), Token::Comma) {
8481                self.advance();
8482            } else {
8483                break;
8484            }
8485        }
8486        Ok(roles)
8487    }
8488
8489    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8490    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8491    /// `CREATE POLICY`.
8492    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8493        use crate::ast::PolicyCmd;
8494        let name = self.expect_ident_like()?;
8495        if !matches!(self.peek(), Token::On) {
8496            return Err(self.err(alloc::format!(
8497                "expected ON after CREATE POLICY name, got {:?}",
8498                self.peek()
8499            )));
8500        }
8501        self.advance();
8502        let table = self.expect_ident_like()?;
8503
8504        let mut permissive = true;
8505        if matches!(self.peek(), Token::As) {
8506            self.advance();
8507            let w = self.expect_ident_like()?;
8508            permissive = if w.eq_ignore_ascii_case("permissive") {
8509                true
8510            } else if w.eq_ignore_ascii_case("restrictive") {
8511                false
8512            } else {
8513                return Err(self.err(alloc::format!(
8514                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8515                )));
8516            };
8517        }
8518
8519        let mut cmd = PolicyCmd::All;
8520        if matches!(self.peek(), Token::For) {
8521            self.advance();
8522            cmd = self.parse_policy_cmd()?;
8523        }
8524
8525        let mut roles = Vec::new();
8526        if matches!(self.peek(), Token::To) {
8527            self.advance();
8528            roles = self.parse_policy_roles()?;
8529        }
8530
8531        let mut using = None;
8532        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8533        {
8534            self.advance();
8535            using = Some(self.parse_paren_expr("USING")?);
8536        }
8537
8538        let mut with_check = None;
8539        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8540        {
8541            self.advance();
8542            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8543            {
8544                return Err(self.err(alloc::format!(
8545                    "expected CHECK after WITH, got {:?}",
8546                    self.peek()
8547                )));
8548            }
8549            self.advance();
8550            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8551        }
8552
8553        // Clause-per-command matrix (PG wording).
8554        match cmd {
8555            PolicyCmd::Insert => {
8556                if using.is_some() {
8557                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8558                }
8559            }
8560            PolicyCmd::Select | PolicyCmd::Delete => {
8561                if with_check.is_some() {
8562                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8563                }
8564            }
8565            PolicyCmd::Update | PolicyCmd::All => {}
8566        }
8567
8568        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8569            name,
8570            table,
8571            permissive,
8572            cmd,
8573            roles,
8574            using,
8575            with_check,
8576        }))
8577    }
8578
8579    /// v7.39 (RLS) — the command word after `FOR`.
8580    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8581        use crate::ast::PolicyCmd;
8582        match self.peek().clone() {
8583            Token::All => {
8584                self.advance();
8585                Ok(PolicyCmd::All)
8586            }
8587            Token::Select => {
8588                self.advance();
8589                Ok(PolicyCmd::Select)
8590            }
8591            Token::Insert => {
8592                self.advance();
8593                Ok(PolicyCmd::Insert)
8594            }
8595            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8596                self.advance();
8597                Ok(PolicyCmd::Update)
8598            }
8599            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8600                self.advance();
8601                Ok(PolicyCmd::Delete)
8602            }
8603            other => Err(self.err(alloc::format!(
8604                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8605            ))),
8606        }
8607    }
8608
8609    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8610    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8611    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8612        let name = self.expect_ident_like()?;
8613        if !matches!(self.peek(), Token::On) {
8614            return Err(self.err(alloc::format!(
8615                "expected ON after ALTER POLICY name, got {:?}",
8616                self.peek()
8617            )));
8618        }
8619        self.advance();
8620        let table = self.expect_ident_like()?;
8621
8622        // RENAME TO new
8623        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8624        {
8625            self.advance();
8626            if !matches!(self.peek(), Token::To) {
8627                return Err(self.err(alloc::format!(
8628                    "expected TO after RENAME, got {:?}",
8629                    self.peek()
8630                )));
8631            }
8632            self.advance();
8633            let new = self.expect_ident_like()?;
8634            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8635                name,
8636                table,
8637                rename_to: Some(new),
8638                roles: None,
8639                using: None,
8640                with_check: None,
8641            }));
8642        }
8643
8644        let mut roles = None;
8645        if matches!(self.peek(), Token::To) {
8646            self.advance();
8647            roles = Some(self.parse_policy_roles()?);
8648        }
8649        let mut using = None;
8650        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8651        {
8652            self.advance();
8653            using = Some(self.parse_paren_expr("USING")?);
8654        }
8655        let mut with_check = None;
8656        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8657        {
8658            self.advance();
8659            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8660            {
8661                return Err(self.err(alloc::format!(
8662                    "expected CHECK after WITH, got {:?}",
8663                    self.peek()
8664                )));
8665            }
8666            self.advance();
8667            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8668        }
8669        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8670            name,
8671            table,
8672            rename_to: None,
8673            roles,
8674            using,
8675            with_check,
8676        }))
8677    }
8678
8679    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8680    /// `DROP POLICY`.
8681    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8682        let if_exists = self.consume_if_exists();
8683        let name = self.expect_ident_like()?;
8684        if !matches!(self.peek(), Token::On) {
8685            return Err(self.err(alloc::format!(
8686                "expected ON after DROP POLICY name, got {:?}",
8687                self.peek()
8688            )));
8689        }
8690        self.advance();
8691        let table = self.expect_ident_like()?;
8692        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8693            name,
8694            table,
8695            if_exists,
8696        }))
8697    }
8698}
8699fn wrap_from_leaves(
8700    e: &mut Expr,
8701    names: &[String],
8702    make: &dyn Fn(Expr) -> Expr,
8703    refs: &dyn Fn(&Expr) -> bool,
8704) {
8705    if let Expr::Column(c) = e {
8706        if c.qualifier
8707            .as_deref()
8708            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8709        {
8710            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8711            *e = make(taken);
8712        }
8713        return;
8714    }
8715    match e {
8716        Expr::Binary { lhs, rhs, .. } => {
8717            wrap_from_leaves(lhs, names, make, refs);
8718            wrap_from_leaves(rhs, names, make, refs);
8719        }
8720        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8721            wrap_from_leaves(expr, names, make, refs)
8722        }
8723        Expr::FunctionCall { args, .. } => {
8724            for a in args.iter_mut() {
8725                wrap_from_leaves(a, names, make, refs);
8726            }
8727        }
8728        Expr::Case {
8729            operand,
8730            branches,
8731            else_branch,
8732        } => {
8733            if let Some(o) = operand.as_deref_mut() {
8734                wrap_from_leaves(o, names, make, refs);
8735            }
8736            for (w, t) in branches.iter_mut() {
8737                wrap_from_leaves(w, names, make, refs);
8738                wrap_from_leaves(t, names, make, refs);
8739            }
8740            if let Some(el) = else_branch.as_deref_mut() {
8741                wrap_from_leaves(el, names, make, refs);
8742            }
8743        }
8744        // Compound variants the walk doesn't decompose: keep the
8745        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8746        // a source table, so nothing regresses.
8747        other => {
8748            if refs(other) {
8749                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8750                *other = make(taken);
8751            }
8752        }
8753    }
8754}
8755
8756/// v7.39 (round 241) — does this expression reference any of the FROM /
8757/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8758/// lowerings)?
8759fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8760    match e {
8761        Expr::Column(c) => c
8762            .qualifier
8763            .as_deref()
8764            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8765        Expr::Binary { lhs, rhs, .. } => {
8766            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8767        }
8768        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8769        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8770        Expr::Case {
8771            operand,
8772            branches,
8773            else_branch,
8774        } => {
8775            operand
8776                .as_deref()
8777                .is_some_and(|o| expr_refs_tables(o, names))
8778                || branches
8779                    .iter()
8780                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8781                || else_branch
8782                    .as_deref()
8783                    .is_some_and(|el| expr_refs_tables(el, names))
8784        }
8785        _ => false,
8786    }
8787}
8788
8789impl Parser {
8790    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8791    /// Caller already consumed the leading `UPDATE` ident.
8792    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8793    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8794    /// after the target name has been read. `JOIN` is a reserved token;
8795    /// the qualifiers are bare idents.
8796    fn peek_is_update_join_start(&self) -> bool {
8797        match self.peek() {
8798            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8799            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8800            Token::Join
8801            | Token::Inner
8802            | Token::Left
8803            | Token::Right
8804            | Token::Cross
8805            | Token::Full => true,
8806            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8807            Token::Ident(s) | Token::QuotedIdent(s) => {
8808                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8809            }
8810            _ => false,
8811        }
8812    }
8813
8814    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8815    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8816    /// expression on the right, and `:=` as a second spelling of `=`.
8817    ///
8818    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8819    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8820    /// and holding this loop's `Vec` + `String` locals there overflowed the
8821    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8822    #[inline(never)]
8823    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8824        let mut assigns: Vec<(String, Expr)> = Vec::new();
8825        let mut settings: Vec<(String, Expr)> = Vec::new();
8826        loop {
8827            // v7.39 (round 554) — a plain NAME here is a session
8828            // setting, not a user variable. mysqldump writes the two in
8829            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8830            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8831            // changes it — and this refused the mixture outright, so no
8832            // dump could be restored past its preamble.
8833            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8834                self.advance();
8835                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8836                    return Err(self.err(alloc::format!(
8837                        "expected `=` after {name}, got {:?}",
8838                        self.peek()
8839                    )));
8840                }
8841                self.advance();
8842                let value = self.parse_expr(0)?;
8843                settings.push((name.to_ascii_lowercase(), value));
8844                if matches!(self.peek(), Token::Comma) {
8845                    self.advance();
8846                    continue;
8847                }
8848                break;
8849            }
8850            let Token::SessionVar(raw) = self.peek().clone() else {
8851                return Err(self.err(alloc::format!(
8852                    "expected a user variable after SET, got {:?}",
8853                    self.peek()
8854                )));
8855            };
8856            if raw.starts_with("@@") {
8857                return Err(self.err(alloc::string::String::from(
8858                    "cannot mix `@@` settings with `@` user variables in one SET",
8859                )));
8860            }
8861            self.advance();
8862            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8863                return Err(self.err(alloc::format!(
8864                    "expected `=` or `:=` after {raw}, got {:?}",
8865                    self.peek()
8866                )));
8867            }
8868            self.advance();
8869            let value = self.parse_expr(0)?;
8870            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8871            if matches!(self.peek(), Token::Comma) {
8872                self.advance();
8873                continue;
8874            }
8875            break;
8876        }
8877        Ok(Statement::SetUserVars(assigns, settings))
8878    }
8879
8880    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8881        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8882        // NAMED `only` until now, which failed on `relation "only" does
8883        // not exist`. The lookahead is what keeps a table actually
8884        // called `only` working: the keyword is only a keyword when a
8885        // TABLE NAME follows it — and `SET` arrives as an identifier
8886        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8887        // for the table and die on the `=`. Measured by the pin.
8888        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8889            if s.eq_ignore_ascii_case("only"))
8890            && matches!(
8891                self.tokens.get(self.pos + 1),
8892                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8893            );
8894        if only {
8895            self.advance();
8896        }
8897        let table = self.expect_ident_like()?;
8898        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8899        // bare spelling; a bare identifier that is the SET keyword itself
8900        // is the clause, not an alias.
8901        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8902        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8903        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8904        // following JOIN a syntax error.
8905        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8906        let alias = if matches!(self.peek(), Token::As) {
8907            self.advance();
8908            Some(self.expect_ident_like()?)
8909        } else {
8910            match self.peek() {
8911                Token::Ident(s) | Token::QuotedIdent(s)
8912                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
8913                {
8914                    let a = s.clone();
8915                    self.advance();
8916                    Some(a)
8917                }
8918                _ => None,
8919            }
8920        };
8921        // v7.39 (round 420) — MySQL's multi-table UPDATE:
8922        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
8923        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
8924        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8925        // The FIRST table is the mutation target and the rest are sources —
8926        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8927        // SPG already lowers onto correlated subqueries. So rewind, let
8928        // `parse_from_clause` read the whole list (it handles aliases, comma
8929        // lists, and every JOIN form), then peel the target off the front.
8930        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8931            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8932        {
8933            // NOTE: `advance()` destroys the tokens it returns
8934            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8935            // is NOT possible — the tail is read forward, once, through the
8936            // same grammar `parse_from_clause` uses after its primary.
8937            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8938            let mut joins = self.parse_from_joins(&target_qual)?;
8939            if joins.is_empty() {
8940                return Err(self.err(alloc::string::String::from(
8941                    "multi-table UPDATE needs at least one source table",
8942                )));
8943            }
8944            let head = joins.remove(0);
8945            // A LEFT join keeps every target row (the unmatched ones see NULL
8946            // on the source side), so it must NOT get the EXISTS row filter
8947            // the inner / comma forms use.
8948            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8949            let src = FromClause {
8950                primary: head.table,
8951                joins,
8952            };
8953            (Some(src), head.on, outer)
8954        } else {
8955            (None, None, false)
8956        };
8957        self.expect_keyword_ident("set")?;
8958        let mut assignments = Vec::new();
8959        loop {
8960            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8961            // …)` — the parenthesized multi-assignment. Expressions
8962            // assign positionally; a subquery RHS clones per column
8963            // keeping only the Nth projection item.
8964            if matches!(self.peek(), Token::LParen) {
8965                self.advance();
8966                let mut cols = alloc::vec![self.expect_ident_like()?];
8967                while matches!(self.peek(), Token::Comma) {
8968                    self.advance();
8969                    cols.push(self.expect_ident_like()?);
8970                }
8971                if !matches!(self.peek(), Token::RParen) {
8972                    return Err(self.err(format!(
8973                        "expected ')' after SET column list, got {:?}",
8974                        self.peek()
8975                    )));
8976                }
8977                self.advance();
8978                if !matches!(self.peek(), Token::Eq) {
8979                    return Err(self.err(format!(
8980                        "expected `=` after SET column list, got {:?}",
8981                        self.peek()
8982                    )));
8983                }
8984                self.advance();
8985                if !matches!(self.peek(), Token::LParen) {
8986                    return Err(self.err(format!(
8987                        "expected '(' after SET (…) =, got {:?}",
8988                        self.peek()
8989                    )));
8990                }
8991                self.advance();
8992                if matches!(self.peek(), Token::Select) {
8993                    let inner = match self.parse_select_stmt()? {
8994                        Statement::Select(s) => s,
8995                        other => {
8996                            return Err(self.err(alloc::format!(
8997                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8998                            )));
8999                        }
9000                    };
9001                    if !matches!(self.peek(), Token::RParen) {
9002                        return Err(self.err(format!(
9003                            "expected ')' after SET subquery, got {:?}",
9004                            self.peek()
9005                        )));
9006                    }
9007                    self.advance();
9008                    if inner.items.len() != cols.len() {
9009                        return Err(self.err(alloc::format!(
9010                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9011                            cols.len(),
9012                            inner.items.len()
9013                        )));
9014                    }
9015                    for (i, col) in cols.into_iter().enumerate() {
9016                        let mut sub = inner.clone();
9017                        sub.items = alloc::vec![sub.items[i].clone()];
9018                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9019                    }
9020                } else {
9021                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9022                    while matches!(self.peek(), Token::Comma) {
9023                        self.advance();
9024                        exprs.push(self.parse_expr(0)?);
9025                    }
9026                    if !matches!(self.peek(), Token::RParen) {
9027                        return Err(self.err(format!(
9028                            "expected ')' after SET row values, got {:?}",
9029                            self.peek()
9030                        )));
9031                    }
9032                    self.advance();
9033                    if exprs.len() != cols.len() {
9034                        return Err(self.err(alloc::format!(
9035                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9036                            cols.len(),
9037                            exprs.len()
9038                        )));
9039                    }
9040                    for (col, e) in cols.into_iter().zip(exprs) {
9041                        assignments.push((col, e));
9042                    }
9043                }
9044                if matches!(self.peek(), Token::Comma) {
9045                    self.advance();
9046                    continue;
9047                }
9048                break;
9049            }
9050            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9051            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9052            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9053            // `public.` dump qualifiers), so the qualifier has to be read off
9054            // the token stream first — otherwise `SET b.v = 888` would write
9055            // to the TARGET table's `v` while naming a source table, a
9056            // silent-wrong. A qualifier naming a SOURCE table means a
9057            // multi-TARGET update — mutating two tables in one statement —
9058            // which SPG does not model, so it is refused loudly.
9059            let set_qual: Option<String> = if mysql_from.is_some()
9060                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9061            {
9062                match self.peek() {
9063                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9064                    _ => None,
9065                }
9066            } else {
9067                None
9068            };
9069            let col = self.expect_ident_like()?;
9070            if let Some(q) = set_qual {
9071                let names_target = q.eq_ignore_ascii_case(&table)
9072                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9073                if !names_target {
9074                    return Err(self.err(alloc::format!(
9075                        "multi-table UPDATE can only assign to its first table \
9076                         ({table}); `{q}.{col}` targets another table"
9077                    )));
9078                }
9079            }
9080            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9081            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9082            // `__column_default` marker lowering just below). PG assigns to the
9083            // i-th (1-based) element, NULL-padding when i exceeds the length.
9084            if matches!(self.peek(), Token::LBracket) {
9085                self.advance();
9086                let index = self.parse_expr(0)?;
9087                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9088                // (and the open `arr[lo:]`), lowered to
9089                // `__array_assign_slice`. Only the single-subscript form
9090                // parsed before, so a slice assignment was a syntax error.
9091                let mut slice_hi: Option<Option<Expr>> = None;
9092                if matches!(self.peek(), Token::Colon) {
9093                    self.advance();
9094                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9095                        None
9096                    } else {
9097                        Some(self.parse_expr(0)?)
9098                    });
9099                }
9100                if !matches!(self.peek(), Token::RBracket) {
9101                    return Err(self.err(format!(
9102                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9103                        self.peek()
9104                    )));
9105                }
9106                self.advance();
9107                if !matches!(self.peek(), Token::Eq) {
9108                    return Err(self.err(format!(
9109                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9110                        self.peek()
9111                    )));
9112                }
9113                self.advance();
9114                let value = self.parse_expr(0)?;
9115                // PG merges several subscript writes to the same column into one
9116                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9117                // assignment to `col` rather than each overwriting the original.
9118                let existing = assignments.iter().position(|(c, _)| c == &col);
9119                let base = match existing {
9120                    Some(i) => assignments[i].1.clone(),
9121                    None => Expr::Column(ColumnName {
9122                        qualifier: None,
9123                        name: col.clone(),
9124                    }),
9125                };
9126                let assigned = match slice_hi {
9127                    None => Expr::FunctionCall {
9128                        name: "__array_assign".to_string(),
9129                        args: alloc::vec![base, index, value],
9130                    },
9131                    Some(hi) => Expr::FunctionCall {
9132                        name: "__array_assign_slice".to_string(),
9133                        args: alloc::vec![
9134                            base,
9135                            index,
9136                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9137                            value,
9138                        ],
9139                    },
9140                };
9141                match existing {
9142                    Some(i) => assignments[i].1 = assigned,
9143                    None => assignments.push((col, assigned)),
9144                }
9145                if matches!(self.peek(), Token::Comma) {
9146                    self.advance();
9147                    continue;
9148                }
9149                break;
9150            }
9151            if !matches!(self.peek(), Token::Eq) {
9152                return Err(self.err(format!(
9153                    "expected `=` after column name in UPDATE SET, got {:?}",
9154                    self.peek()
9155                )));
9156            }
9157            self.advance();
9158            // `SET col = DEFAULT` — the column's declared default;
9159            // rides out as a marker call the update executor
9160            // resolves against the schema.
9161            let value = if matches!(self.peek(), Token::Default) {
9162                self.advance();
9163                Expr::FunctionCall {
9164                    name: "__column_default".to_string(),
9165                    args: Vec::new(),
9166                }
9167            } else {
9168                self.parse_expr(0)?
9169            };
9170            assignments.push((col, value));
9171            if matches!(self.peek(), Token::Comma) {
9172                self.advance();
9173                continue;
9174            }
9175            break;
9176        }
9177        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9178        // update. Lowers onto the correlated-subquery machinery:
9179        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9180        // and each assignment that references a FROM-list table
9181        // wraps into a correlated scalar subquery
9182        // (SELECT expr FROM src WHERE cond). Equivalent for the
9183        // unique-join shape (the overwhelmingly common one); a
9184        // multi-match, which PG resolves by arbitrary pick,
9185        // surfaces as a scalar-subquery cardinality error instead
9186        // of a silent arbitrary result.
9187        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9188        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9189        // the SAME lowering below. Both spellings together is not legal in
9190        // either dialect.
9191        let from_clause = if let Some(fc) = mysql_from {
9192            if matches!(self.peek(), Token::From) {
9193                return Err(self.err(alloc::string::String::from(
9194                    "multi-table UPDATE already names its sources; drop the FROM clause",
9195                )));
9196            }
9197            Some(fc)
9198        } else if matches!(self.peek(), Token::From) {
9199            self.advance();
9200            Some(self.parse_from_clause()?)
9201        } else {
9202            None
9203        };
9204        let where_ = if matches!(self.peek(), Token::Where) {
9205            self.advance();
9206            Some(self.parse_expr(0)?)
9207        } else {
9208            None
9209        };
9210        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9211        // and the TARGET-row filter are NOT the same predicate once a LEFT
9212        // join is involved:
9213        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9214        //     one conjunction, and the whole thing filters target rows via
9215        //     EXISTS.
9216        //   * LEFT join: only the ON predicate belongs inside the source
9217        //     subquery. The WHERE still filters TARGET rows (with source
9218        //     columns read through the correlated subquery, which yields NULL
9219        //     for an unmatched row — exactly LEFT-join semantics).
9220        // Round 420 folded ON into WHERE unconditionally and then dropped the
9221        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9222        // WHERE a.id > 1` updated EVERY row.
9223        let sub_where = match (mysql_on.clone(), where_.clone()) {
9224            _ if mysql_outer => mysql_on.clone(),
9225            (Some(on), Some(w)) => Some(Expr::Binary {
9226                lhs: Box::new(on),
9227                op: crate::ast::BinOp::And,
9228                rhs: Box::new(w),
9229            }),
9230            (Some(on), None) => Some(on),
9231            (None, w) => w,
9232        };
9233        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9234        // has no such clause on UPDATE, so this is accepted only under the
9235        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9236        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9237        let mut returning = self.parse_optional_returning()?;
9238        // v7.39 (round 533) — kept for the engine, which can resolve the
9239        // UNQUALIFIED leaves this lowering has to leave alone.
9240        let from_sources = from_clause.as_ref().map(|fc| {
9241            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9242                from: fc.clone(),
9243                sub_where: sub_where.clone(),
9244            })
9245        });
9246        let (assignments, where_) = if let Some(fc) = from_clause {
9247            let names: Vec<String> = core::iter::once(&fc.primary)
9248                .chain(fc.joins.iter().map(|j| &j.table))
9249                .flat_map(|t| {
9250                    t.alias
9251                        .clone()
9252                        .into_iter()
9253                        .chain(core::iter::once(t.name.clone()))
9254                })
9255                .collect();
9256            let refs_list = |e: &Expr| -> bool {
9257                fn walk(e: &Expr, names: &[String]) -> bool {
9258                    match e {
9259                        Expr::Column(c) => c
9260                            .qualifier
9261                            .as_deref()
9262                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9263                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9264                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9265                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9266                        Expr::Case {
9267                            operand,
9268                            branches,
9269                            else_branch,
9270                        } => {
9271                            operand.as_deref().is_some_and(|o| walk(o, names))
9272                                || branches
9273                                    .iter()
9274                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9275                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9276                        }
9277                        _ => false,
9278                    }
9279                }
9280                walk(e, &names)
9281            };
9282            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9283                locking: None,
9284                ctes: Vec::new(),
9285                distinct: false,
9286                distinct_on: Vec::new(),
9287                items,
9288                from: Some(fc.clone()),
9289                where_: sub_where.clone(),
9290                group_by: None,
9291                group_by_all: false,
9292                having: None,
9293                unions: Vec::new(),
9294                order_by: Vec::new(),
9295                limit: None,
9296                offset: None,
9297                limit_with_ties: false,
9298                window_check_exprs: Vec::new(),
9299            };
9300            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9301            // assignment RHS with a correlated scalar subquery, instead of
9302            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9303            // column reference (`SET v = v + u.bonus`, where `v` is the target
9304            // table's column) inside a subquery whose FROM only has the source
9305            // table, so the unqualified `v` resolved against the source and
9306            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9307            // context — where they belong — fixes it; only the source columns
9308            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9309            // compound variants the leaf-walk doesn't decompose.
9310            let make_subq = |inner: Expr| {
9311                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9312                    expr: inner,
9313                    alias: None,
9314                }])))
9315            };
9316            let assignments = assignments
9317                .into_iter()
9318                .map(|(col, mut expr)| {
9319                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9320                    (col, expr)
9321                })
9322                .collect();
9323            let exists = Expr::Exists {
9324                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9325                    expr: Expr::Literal(Literal::Integer(1)),
9326                    alias: None,
9327                }])),
9328                negated: false,
9329            };
9330            // v7.39 (round 241) — RETURNING may reference the FROM-list
9331            // tables too (`RETURNING emp.id, dept.name`); the same
9332            // leaf-to-correlated-subquery lowering the assignments get.
9333            // Without it the qualifier died at eval with "unknown table
9334            // qualifier". (RETURNING was parsed before this block — the
9335            // lowering is a pure AST transformation.)
9336            if let Some(items) = returning.as_mut() {
9337                for item in items.iter_mut() {
9338                    if let SelectItem::Expr { expr, .. } = item {
9339                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9340                    }
9341                }
9342            }
9343            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9344            // EVERY matching target row: it gets no EXISTS filter, but the
9345            // caller's WHERE still applies, with source columns read through
9346            // the correlated subquery (NULL when unmatched — LEFT-join
9347            // semantics). `sub_where` above already excluded the WHERE from
9348            // the source subquery for this case.
9349            if mysql_outer {
9350                let mut outer = where_;
9351                if let Some(w) = outer.as_mut() {
9352                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9353                }
9354                (assignments, outer)
9355            } else {
9356                (assignments, Some(exists))
9357            }
9358        } else {
9359            (assignments, where_)
9360        };
9361        Ok(Statement::Update(crate::ast::UpdateStatement {
9362            ctes: Vec::new(),
9363            table,
9364            only,
9365            alias,
9366            assignments,
9367            from_sources,
9368            where_,
9369            order_limit: update_order_limit,
9370            returning,
9371        }))
9372    }
9373
9374    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9375    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9376    /// clause and its meaning are identical, so both call this rather than
9377    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9378    /// legal. PG has no such clause on either statement, so it is read only
9379    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9380    /// errors.
9381    ///
9382    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9383    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9384    /// stack in round 430.
9385    #[inline(never)]
9386    fn parse_mysql_dml_order_limit(
9387        &mut self,
9388        what: &str,
9389    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9390        if !self.mysql_dialect {
9391            return Ok(None);
9392        }
9393        let order_by = self.parse_order_by_keys()?;
9394        let limit = if matches!(self.peek(), Token::Limit) {
9395            self.advance();
9396            let tok = self.advance();
9397            let Token::Integer(n) = tok else {
9398                return Err(self.err(alloc::format!(
9399                    "expected integer after {what} LIMIT, got {tok:?}"
9400                )));
9401            };
9402            // MySQL rejects the `LIMIT offset, count` form here — only a
9403            // single row count is legal on a DML statement.
9404            if matches!(self.peek(), Token::Comma) {
9405                return Err(self.err(alloc::format!(
9406                    "{what} LIMIT takes a row count, not an offset"
9407                )));
9408            }
9409            let n = u32::try_from(n)
9410                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9411            Some(n)
9412        } else {
9413            None
9414        };
9415        if order_by.is_empty() && limit.is_none() {
9416            return Ok(None);
9417        }
9418        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9419            order_by,
9420            limit,
9421        })))
9422    }
9423
9424    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9425    /// the leading `DELETE` ident.
9426    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9427        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9428        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9429        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9430        // parse here; it reaches the existing USING path with the target
9431        // repeated in the list, which the source-list peel below handles.)
9432        // More than one name is a multi-TARGET delete, which SPG does not
9433        // model; it is refused rather than half-applied.
9434        let mysql_pre_target: Option<String> =
9435            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9436                let first = self.expect_ident_like()?;
9437                if matches!(self.peek(), Token::Comma) {
9438                    return Err(self.err(alloc::format!(
9439                        "multi-table DELETE can only delete from one table; \
9440                     `DELETE {first}, …` names several"
9441                    )));
9442                }
9443                Some(first)
9444            } else {
9445                None
9446            };
9447        if !matches!(self.peek(), Token::From) {
9448            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9449        }
9450        self.advance();
9451        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9452        // lookahead as the UPDATE spelling.
9453        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9454            if s.eq_ignore_ascii_case("only"))
9455            && matches!(
9456                self.tokens.get(self.pos + 1),
9457                Some(Token::Ident(_) | Token::QuotedIdent(_))
9458            );
9459        if only {
9460            self.advance();
9461        }
9462        let table = self.expect_ident_like()?;
9463        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9464        // spelling must not swallow the clause keywords that can follow
9465        // the target.
9466        let alias = if matches!(self.peek(), Token::As) {
9467            self.advance();
9468            Some(self.expect_ident_like()?)
9469        } else {
9470            match self.peek() {
9471                Token::Ident(s) | Token::QuotedIdent(s)
9472                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9473                {
9474                    let a = s.clone();
9475                    self.advance();
9476                    Some(a)
9477                }
9478                _ => None,
9479            }
9480        };
9481        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9482        // through the SAME join grammar the FROM clause uses (see the
9483        // `advance()`-destroys-tokens note on `parse_from_joins`).
9484        let mut mysql_on: Option<Expr> = None;
9485        let mut mysql_outer = false;
9486        let mysql_using = if mysql_pre_target.is_some()
9487            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9488        {
9489            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9490            let mut joins = self.parse_from_joins(&target_qual)?;
9491            if joins.is_empty() {
9492                return Err(self.err(alloc::string::String::from(
9493                    "multi-table DELETE needs at least one source table",
9494                )));
9495            }
9496            let head = joins.remove(0);
9497            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9498            mysql_on = head.on;
9499            Some(FromClause {
9500                primary: head.table,
9501                joins,
9502            })
9503        } else {
9504            None
9505        };
9506        // The pre-FROM target must be the table the FROM names (or its
9507        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9508        // is not the scan target.
9509        if let Some(t) = &mysql_pre_target {
9510            let names_target = t.eq_ignore_ascii_case(&table)
9511                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9512            if !names_target {
9513                return Err(self.err(alloc::format!(
9514                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9515                )));
9516            }
9517        }
9518        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9519        // delete. Same lowering as UPDATE … FROM: the WHERE
9520        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9521        // target row by the correlated machinery.
9522        let using_clause = if let Some(fc) = mysql_using {
9523            Some(fc)
9524        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9525            self.advance();
9526            let mut fc = self.parse_from_clause()?;
9527            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9528            // repeats the TARGET as the first USING entry (PG's spelling
9529            // lists only the extra sources). Peel it so the source subquery
9530            // does not re-scan — and shadow — the target table.
9531            let primary_is_target =
9532                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9533            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9534                let head = fc.joins.remove(0);
9535                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9536                mysql_on = head.on;
9537                fc = FromClause {
9538                    primary: head.table,
9539                    joins: fc.joins,
9540                };
9541            }
9542            Some(fc)
9543        } else {
9544            None
9545        };
9546        let where_ = if matches!(self.peek(), Token::Where) {
9547            self.advance();
9548            Some(self.parse_expr(0)?)
9549        } else {
9550            None
9551        };
9552        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9553        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9554        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9555        let mut returning = self.parse_optional_returning()?;
9556        let where_ = if let Some(fc) = using_clause {
9557            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9558            // a USING-table reference in RETURNING becomes a correlated
9559            // scalar subquery over the USING list.
9560            let names: Vec<String> = core::iter::once(&fc.primary)
9561                .chain(fc.joins.iter().map(|j| &j.table))
9562                .flat_map(|t| {
9563                    t.alias
9564                        .clone()
9565                        .into_iter()
9566                        .chain(core::iter::once(t.name.clone()))
9567                })
9568                .collect();
9569            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9570            // join filters the SOURCE subquery on the ON predicate alone and
9571            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9572            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9573            // rows); every other form folds ON and WHERE into one EXISTS.
9574            let sub_where = match (mysql_on.clone(), where_.clone()) {
9575                _ if mysql_outer => mysql_on.clone(),
9576                (Some(on), Some(w)) => Some(Expr::Binary {
9577                    lhs: Box::new(on),
9578                    op: crate::ast::BinOp::And,
9579                    rhs: Box::new(w),
9580                }),
9581                (Some(on), None) => Some(on),
9582                (None, w) => w,
9583            };
9584            let exists_where = sub_where.clone();
9585            let sub_fc = fc.clone();
9586            let make_subq = move |leaf: Expr| -> Expr {
9587                Expr::ScalarSubquery(Box::new(SelectStatement {
9588                    locking: None,
9589                    ctes: Vec::new(),
9590                    distinct: false,
9591                    distinct_on: Vec::new(),
9592                    items: alloc::vec![SelectItem::Expr {
9593                        expr: leaf,
9594                        alias: None,
9595                    }],
9596                    from: Some(sub_fc.clone()),
9597                    where_: sub_where.clone(),
9598                    group_by: None,
9599                    group_by_all: false,
9600                    having: None,
9601                    unions: Vec::new(),
9602                    order_by: Vec::new(),
9603                    limit: None,
9604                    offset: None,
9605                    limit_with_ties: false,
9606                    window_check_exprs: Vec::new(),
9607                }))
9608            };
9609            let refs = |e: &Expr| expr_refs_tables(e, &names);
9610            if let Some(items) = returning.as_mut() {
9611                for item in items.iter_mut() {
9612                    if let SelectItem::Expr { expr, .. } = item {
9613                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9614                    }
9615                }
9616            }
9617            // A LEFT join deletes the target rows the WHERE selects, reading
9618            // source columns through the correlated subquery (NULL when
9619            // unmatched); no EXISTS row filter.
9620            if mysql_outer {
9621                let mut outer = where_;
9622                if let Some(w) = outer.as_mut() {
9623                    wrap_from_leaves(w, &names, &make_subq, &refs);
9624                }
9625                outer
9626            } else {
9627                Some(Expr::Exists {
9628                    subquery: Box::new(SelectStatement {
9629                        locking: None,
9630                        ctes: Vec::new(),
9631                        distinct: false,
9632                        distinct_on: Vec::new(),
9633                        items: alloc::vec![SelectItem::Expr {
9634                            expr: Expr::Literal(Literal::Integer(1)),
9635                            alias: None,
9636                        }],
9637                        from: Some(fc),
9638                        where_: exists_where,
9639                        group_by: None,
9640                        group_by_all: false,
9641                        having: None,
9642                        unions: Vec::new(),
9643                        order_by: Vec::new(),
9644                        limit: None,
9645                        offset: None,
9646                        limit_with_ties: false,
9647                        window_check_exprs: Vec::new(),
9648                    }),
9649                    negated: false,
9650                })
9651            }
9652        } else {
9653            where_
9654        };
9655        Ok(Statement::Delete(crate::ast::DeleteStatement {
9656            ctes: Vec::new(),
9657            table,
9658            only,
9659            alias,
9660            where_,
9661            order_limit: delete_order_limit,
9662            returning,
9663        }))
9664    }
9665
9666    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9667    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9668    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9669    /// keyword. v7.17 surface:
9670    ///   * source: table reference (subquery source is a follow-up)
9671    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9672    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9673    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9674    ///     order
9675    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9676        // INTO
9677        let is_into_kw = matches!(self.peek(), Token::Into)
9678            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9679        if !is_into_kw {
9680            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9681        }
9682        self.advance();
9683        let target = self.expect_ident_like()?;
9684        // Optional alias — bare ident before USING.
9685        let target_alias = match self.peek() {
9686            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9687                Some(self.expect_ident_like()?)
9688            }
9689            _ => None,
9690        };
9691        // USING
9692        let is_using_kw = matches!(
9693            self.peek(),
9694            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9695        );
9696        if !is_using_kw {
9697            return Err(self.err(format!(
9698                "expected USING after MERGE INTO target, got {:?}",
9699                self.peek()
9700            )));
9701        }
9702        self.advance();
9703        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9704        // <table> [alias]`. PG requires an alias after a subquery source.
9705        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9706            self.advance(); // (
9707            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9708            // constant-SELECT lowering the derived-table parser uses
9709            // (PG deletes through this form; it was a parse error).
9710            let inner = if matches!(self.peek(), Token::Values) {
9711                self.advance(); // VALUES
9712                Statement::Select(self.parse_values_rows_body()?)
9713            } else {
9714                self.parse_select_stmt()?
9715            };
9716            match self.advance() {
9717                Token::RParen => {}
9718                other => {
9719                    return Err(self.err(format!(
9720                        "expected ')' after MERGE USING subquery, got {other:?}"
9721                    )));
9722                }
9723            }
9724            let Statement::Select(sub) = inner else {
9725                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9726            };
9727            (String::new(), Some(Box::new(sub)))
9728        } else {
9729            (self.expect_ident_like()?, None)
9730        };
9731        let source_alias = match self.peek() {
9732            Token::Ident(s) | Token::QuotedIdent(s)
9733                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9734            {
9735                Some(self.expect_ident_like()?)
9736            }
9737            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9738                self.advance(); // AS
9739                Some(self.expect_ident_like()?)
9740            }
9741            _ => None,
9742        };
9743        // v7.39 (round 768, F31-D5) — optional positional column-alias
9744        // list after the source alias (`s(id, v)`).
9745        let mut source_column_aliases: Vec<String> = Vec::new();
9746        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9747            self.advance();
9748            loop {
9749                source_column_aliases.push(self.expect_ident_like()?);
9750                match self.peek() {
9751                    Token::Comma => {
9752                        self.advance();
9753                    }
9754                    Token::RParen => {
9755                        self.advance();
9756                        break;
9757                    }
9758                    other => {
9759                        return Err(self.err(format!(
9760                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9761                        )));
9762                    }
9763                }
9764            }
9765        }
9766        if source_select.is_some() && source_alias.is_none() {
9767            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9768        }
9769        // ON
9770        if !matches!(self.peek(), Token::On) {
9771            return Err(self.err(format!(
9772                "expected ON after MERGE … USING source, got {:?}",
9773                self.peek()
9774            )));
9775        }
9776        self.advance();
9777        let on = self.parse_expr(0)?;
9778        // One or more WHEN clauses.
9779        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9780        loop {
9781            let is_when_kw = matches!(
9782                self.peek(),
9783                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9784            );
9785            if !is_when_kw {
9786                break;
9787            }
9788            self.advance(); // WHEN
9789            // [NOT] MATCHED
9790            let matched = if matches!(self.peek(), Token::Not) {
9791                self.advance();
9792                crate::ast::MergeMatched::NotMatched
9793            } else {
9794                crate::ast::MergeMatched::Matched
9795            };
9796            let is_matched_kw = matches!(
9797                self.peek(),
9798                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9799            );
9800            if !is_matched_kw {
9801                return Err(self.err(format!(
9802                    "expected MATCHED in WHEN clause, got {:?}",
9803                    self.peek()
9804                )));
9805            }
9806            self.advance();
9807            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9808            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9809            // to fire for target rows no source row matches.
9810            let mut matched = matched;
9811            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9812                self.advance();
9813                match self.peek() {
9814                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9815                        self.advance();
9816                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9817                    }
9818                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9819                        self.advance();
9820                    }
9821                    other => {
9822                        return Err(self.err(format!(
9823                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9824                        )));
9825                    }
9826                }
9827            }
9828            // Optional AND <expr>
9829            let condition = if matches!(self.peek(), Token::And) {
9830                self.advance();
9831                Some(self.parse_expr(0)?)
9832            } else {
9833                None
9834            };
9835            // THEN
9836            let is_then_kw = matches!(
9837                self.peek(),
9838                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9839            );
9840            if !is_then_kw {
9841                return Err(self.err(format!(
9842                    "expected THEN in WHEN clause, got {:?}",
9843                    self.peek()
9844                )));
9845            }
9846            self.advance();
9847            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9848            let action = match self.peek().clone() {
9849                Token::Insert => {
9850                    self.advance();
9851                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9852                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9853                    // VALUES (…)` omits it and fills every column in declaration
9854                    // order. PG accepts this; SPG used to require the list.
9855                    let mut columns: Vec<String> = Vec::new();
9856                    if matches!(self.peek(), Token::LParen) {
9857                        self.advance();
9858                        loop {
9859                            columns.push(self.expect_ident_like()?);
9860                            if matches!(self.peek(), Token::Comma) {
9861                                self.advance();
9862                                continue;
9863                            }
9864                            break;
9865                        }
9866                        if !matches!(self.peek(), Token::RParen) {
9867                            return Err(self.err(format!(
9868                                "expected ')' after INSERT column list, got {:?}",
9869                                self.peek()
9870                            )));
9871                        }
9872                        self.advance();
9873                    }
9874                    // VALUES (...)
9875                    if !matches!(self.peek(), Token::Values) {
9876                        return Err(self.err(format!(
9877                            "expected VALUES in MERGE INSERT, got {:?}",
9878                            self.peek()
9879                        )));
9880                    }
9881                    self.advance();
9882                    if !matches!(self.peek(), Token::LParen) {
9883                        return Err(self.err(format!(
9884                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
9885                            self.peek()
9886                        )));
9887                    }
9888                    self.advance();
9889                    let mut values: Vec<crate::ast::Expr> = Vec::new();
9890                    loop {
9891                        values.push(self.parse_expr(0)?);
9892                        if matches!(self.peek(), Token::Comma) {
9893                            self.advance();
9894                            continue;
9895                        }
9896                        break;
9897                    }
9898                    if !matches!(self.peek(), Token::RParen) {
9899                        return Err(self.err(format!(
9900                            "expected ')' after MERGE INSERT values, got {:?}",
9901                            self.peek()
9902                        )));
9903                    }
9904                    self.advance();
9905                    // Empty column list = positional into every column, so the
9906                    // count is checked against the table arity at execution.
9907                    if !columns.is_empty() && columns.len() != values.len() {
9908                        return Err(self.err(format!(
9909                            "MERGE INSERT column count ({}) ≠ value count ({})",
9910                            columns.len(),
9911                            values.len()
9912                        )));
9913                    }
9914                    crate::ast::MergeAction::Insert { columns, values }
9915                }
9916                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9917                    self.advance();
9918                    // SET
9919                    let is_set_kw = matches!(
9920                        self.peek(),
9921                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9922                    );
9923                    if !is_set_kw {
9924                        return Err(self.err(format!(
9925                            "expected SET after UPDATE in MERGE, got {:?}",
9926                            self.peek()
9927                        )));
9928                    }
9929                    self.advance();
9930                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9931                    loop {
9932                        let col = self.expect_ident_like()?;
9933                        if !matches!(self.peek(), Token::Eq) {
9934                            return Err(self.err(format!(
9935                                "expected '=' in MERGE UPDATE assignment, got {:?}",
9936                                self.peek()
9937                            )));
9938                        }
9939                        self.advance();
9940                        let expr = self.parse_expr(0)?;
9941                        assignments.push((col, expr));
9942                        if matches!(self.peek(), Token::Comma) {
9943                            self.advance();
9944                            continue;
9945                        }
9946                        break;
9947                    }
9948                    crate::ast::MergeAction::Update { assignments }
9949                }
9950                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9951                    self.advance();
9952                    crate::ast::MergeAction::Delete
9953                }
9954                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9955                    self.advance();
9956                    let is_nothing_kw = matches!(
9957                        self.peek(),
9958                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9959                    );
9960                    if !is_nothing_kw {
9961                        return Err(self.err(format!(
9962                            "expected NOTHING after DO in MERGE clause, got {:?}",
9963                            self.peek()
9964                        )));
9965                    }
9966                    self.advance();
9967                    crate::ast::MergeAction::DoNothing
9968                }
9969                other => {
9970                    return Err(self.err(format!(
9971                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9972                    )));
9973                }
9974            };
9975            // PG's grammar simply has no INSERT production under BY SOURCE
9976            // (a target row already exists there) — same syntax error.
9977            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9978                && matches!(action, crate::ast::MergeAction::Insert { .. })
9979            {
9980                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9981            }
9982            clauses.push(crate::ast::MergeWhenClause {
9983                matched,
9984                condition,
9985                action,
9986            });
9987        }
9988        if clauses.is_empty() {
9989            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9990        }
9991        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9992        // unconditional (no `AND`) WHEN of the same match kind: it could
9993        // never fire. Check per match kind in clause order.
9994        let mut seen_unconditional_matched = false;
9995        let mut seen_unconditional_not_matched = false;
9996        let mut seen_unconditional_by_source = false;
9997        for c in &clauses {
9998            let seen = match c.matched {
9999                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10000                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10001                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10002            };
10003            if *seen {
10004                return Err(self.err(String::from(
10005                    "unreachable WHEN clause specified after unconditional WHEN clause",
10006                )));
10007            }
10008            if c.condition.is_none() {
10009                *seen = true;
10010            }
10011        }
10012        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10013        let returning = self.parse_optional_returning()?;
10014        Ok(Statement::Merge(crate::ast::MergeStatement {
10015            // Attached by `parse_with_cte_then_select` when the MERGE
10016            // heads a WITH clause (round 149).
10017            ctes: Vec::new(),
10018            target,
10019            target_alias,
10020            source,
10021            source_alias,
10022            source_select,
10023            source_column_aliases,
10024            on,
10025            clauses,
10026            returning,
10027        }))
10028    }
10029
10030    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10031    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10032    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10033    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10034    fn parse_optional_returning(
10035        &mut self,
10036    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10037        let is_returning_kw = matches!(
10038            self.peek(),
10039            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10040        );
10041        if !is_returning_kw {
10042            return Ok(None);
10043        }
10044        self.advance();
10045        let mut items = Vec::new();
10046        loop {
10047            items.push(self.parse_select_item()?);
10048            if matches!(self.peek(), Token::Comma) {
10049                self.advance();
10050                continue;
10051            }
10052            break;
10053        }
10054        Ok(Some(items))
10055    }
10056
10057    /// v6.0.4 — parse the tail of an ALTER statement after the
10058    /// leading `ALTER` keyword has been consumed. Only one form is
10059    /// supported in v6.0.4:
10060    ///
10061    /// ```text
10062    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10063    /// ```
10064    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10065        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10066        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10067        // exclusion) is accepted by stripping the `ONLY` keyword
10068        // before the table parse.
10069        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10070        // and the long PG-dump tail are accepted as no-ops.
10071        match self.advance() {
10072            Token::Index => {}
10073            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10074            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10075            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10076            Token::Table => {
10077                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10078                    self.advance();
10079                }
10080                return self.parse_alter_table_after_keyword();
10081            }
10082            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10083                return self.parse_alter_policy_after_keyword();
10084            }
10085            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10086                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10087                    self.advance();
10088                }
10089                return self.parse_alter_table_after_keyword();
10090            }
10091            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10092            // of the silent-noop tail.
10093            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10094                return self.parse_alter_sequence_after_keyword();
10095            }
10096            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10097            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10098            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10099            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10100                // NB: the match arm consumed `TYPE` via self.advance(); the
10101                // cursor is now at the type name — do NOT advance again.
10102                let type_name = self.expect_ident_like()?;
10103                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10104                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10105                if is_add_value {
10106                    self.advance(); // ADD
10107                    self.advance(); // VALUE
10108                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10109                    // IF/EXISTS as identifiers.
10110                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10111                    {
10112                        let n1 = self.tokens.get(self.pos + 1);
10113                        let n2 = self.tokens.get(self.pos + 2);
10114                        if matches!(n1, Some(Token::Not))
10115                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10116                        {
10117                            self.advance();
10118                            self.advance();
10119                            self.advance();
10120                            true
10121                        } else {
10122                            false
10123                        }
10124                    } else {
10125                        false
10126                    };
10127                    let label = self.expect_string_literal()?;
10128                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10129                    {
10130                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10131                        self.advance();
10132                        let anchor = self.expect_string_literal()?;
10133                        Some((is_before, anchor))
10134                    } else {
10135                        None
10136                    };
10137                    return Ok(Statement::AlterTypeAddValue {
10138                        type_name,
10139                        label,
10140                        if_not_exists,
10141                        position,
10142                    });
10143                }
10144                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10145                // Used to fall into the no-op tail below: accepted, silently
10146                // ignored. `RENAME TO <newtype>` keeps falling through.
10147                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10148                    && matches!(
10149                        self.tokens.get(self.pos + 1),
10150                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10151                    )
10152                {
10153                    self.advance(); // RENAME
10154                    self.advance(); // VALUE
10155                    let old = self.expect_string_literal()?;
10156                    if matches!(self.peek(), Token::To) {
10157                        self.advance();
10158                    } else {
10159                        self.expect_keyword_ident("to")?;
10160                    }
10161                    let new = self.expect_string_literal()?;
10162                    return Ok(Statement::AlterTypeRenameValue {
10163                        type_name,
10164                        old,
10165                        new,
10166                    });
10167                }
10168                // Other ALTER TYPE forms — the ACTION stays a no-op
10169                // (pg_dump tail), but v7.39 (round 708) the NAME is
10170                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10171                // success for a type that does not exist.
10172                self.consume_until_statement_boundary();
10173                return Ok(Statement::ValidateOnly {
10174                    kind: crate::ast::ValidateOnlyKind::TypeName,
10175                    names: alloc::vec![type_name],
10176                });
10177            }
10178            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10179            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10180            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10181            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10182            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10183            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10184            // pg_dump no-op list below: every form used to report success
10185            // and change nothing, which is worse than refusing outright
10186            // (a migration dropping a constraint kept rejecting data).
10187            // NOTE: the enclosing `match self.advance()` already consumed
10188            // the DOMAIN keyword, so the name is next.
10189            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10190                return self.parse_alter_domain_after_keyword();
10191            }
10192            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10193            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10194            // used to fall into the pg_dump no-op tail below, so a DBA
10195            // setting a per-role default was told it worked and nothing
10196            // happened. Intercepted here, BEFORE that tail.
10197            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10198            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10199            // interception below exists: swallowed with the no-op tail, an
10200            // unknown parameter name was ACCEPTED where PG18 answers
10201            // `unrecognized configuration parameter`. SPG applies nothing
10202            // either way — there is no postgresql.auto.conf — but it now
10203            // says so about a name it does not know.
10204            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10205                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10206                // already consumed here. An extra advance eats the SET and
10207                // the parameter name is never seen — which is exactly the
10208                // bug a panic in this branch disproved: the branch WAS on
10209                // the path, the reading of it was wrong.
10210                let mut parameter = None;
10211                // SET <name> … | RESET <name> | RESET ALL
10212                if matches!(self.peek(), Token::Ident(k)
10213                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10214                {
10215                    self.advance();
10216                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10217                        && !n.eq_ignore_ascii_case("all")
10218                    {
10219                        self.advance();
10220                        // A dotted GUC (`plpgsql.check_asserts`) is two
10221                        // tokens; keep the whole name.
10222                        let mut full = n;
10223                        while matches!(self.peek(), Token::Dot) {
10224                            self.advance();
10225                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10226                                full.push('.');
10227                                full.push_str(&t);
10228                            }
10229                        }
10230                        parameter = Some(full);
10231                    }
10232                }
10233                self.consume_until_statement_boundary();
10234                return Ok(Statement::AlterSystem { parameter });
10235            }
10236            Token::Ident(s) | Token::QuotedIdent(s)
10237                if matches!(
10238                    s.to_ascii_lowercase().as_str(),
10239                    "role" | "user" | "database"
10240                ) && self.peeks_db_role_setting() =>
10241            {
10242                let is_database = s.eq_ignore_ascii_case("database");
10243                return self.parse_db_role_setting(is_database);
10244            }
10245            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10246            // (the non-SET forms; SET/RESET took the branch above). The
10247            // attributes still no-op — recorded, and the ignored PASSWORD
10248            // is ledgered as its own follow-up — but the ROLE is validated:
10249            // any name was accepted for a role that does not exist.
10250            Token::Ident(s) | Token::QuotedIdent(s)
10251                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10252            {
10253                // NB: the enclosing `match self.advance()` already consumed
10254                // ROLE/USER — the round-695 trap, hit again in this round's
10255                // first draft (the name was eaten and WITH parsed as the
10256                // role). The cursor is at the name.
10257                let name = self.expect_ident_or_string()?;
10258                // v7.39 (round 750) — scan the attribute tail for
10259                // PASSWORD. Everything else stays a recorded no-op, but
10260                // a dropped credential rotation is a SECURITY bug:
10261                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10262                // changed nothing, so the old password kept working.
10263                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10264                // NULL` clears the credential.
10265                let mut password: Option<Option<String>> = None;
10266                loop {
10267                    match self.peek() {
10268                        Token::Semicolon | Token::Eof => break,
10269                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10270                            self.advance();
10271                            match self.advance() {
10272                                Token::String(p) => password = Some(Some(p)),
10273                                Token::Null => password = Some(None),
10274                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10275                                    password = Some(None);
10276                                }
10277                                other => {
10278                                    return Err(self.err(alloc::format!(
10279                                        "expected password string or NULL after PASSWORD, got {other:?}"
10280                                    )));
10281                                }
10282                            }
10283                        }
10284                        _ => {
10285                            self.advance();
10286                        }
10287                    }
10288                }
10289                if name.eq_ignore_ascii_case("all") {
10290                    // `ALTER ROLE ALL …` names every role; nothing to check.
10291                    return Ok(Statement::Empty);
10292                }
10293                if let Some(pw) = password {
10294                    return Ok(Statement::AlterRolePassword { name, password: pw });
10295                }
10296                return Ok(Statement::ValidateOnly {
10297                    kind: crate::ast::ValidateOnlyKind::RoleName,
10298                    names: alloc::vec![name],
10299                });
10300            }
10301            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10302            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10303            // list far enough to validate the NAME; the actions still no-op.
10304            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10305            // models none of them and their dumps are rare.)
10306            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10307                let name = self.expect_ident_or_string()?;
10308                self.consume_until_statement_boundary();
10309                return Ok(Statement::ValidateOnly {
10310                    kind: crate::ast::ValidateOnlyKind::CollationName,
10311                    names: alloc::vec![name],
10312                });
10313            }
10314            Token::Ident(s) | Token::QuotedIdent(s)
10315                if s.eq_ignore_ascii_case("text")
10316                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10317                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10318            {
10319                self.advance(); // SEARCH
10320                self.advance(); // CONFIGURATION
10321                let name = self.expect_ident_like()?;
10322                self.consume_until_statement_boundary();
10323                return Ok(Statement::ValidateOnly {
10324                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10325                    names: alloc::vec![name],
10326                });
10327            }
10328            Token::Ident(s) | Token::QuotedIdent(s)
10329                if s.eq_ignore_ascii_case("event")
10330                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10331            {
10332                self.advance(); // TRIGGER
10333                let name = self.expect_ident_like()?;
10334                self.consume_until_statement_boundary();
10335                return Ok(Statement::ValidateOnly {
10336                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10337                    names: alloc::vec![name],
10338                });
10339            }
10340            Token::Ident(s) | Token::QuotedIdent(s)
10341                if s.eq_ignore_ascii_case("large")
10342                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10343            {
10344                self.advance(); // OBJECT
10345                let oid = match self.advance() {
10346                    Token::Integer(n) => alloc::format!("{n}"),
10347                    other => {
10348                        return Err(
10349                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10350                        );
10351                    }
10352                };
10353                self.consume_until_statement_boundary();
10354                return Ok(Statement::ValidateOnly {
10355                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10356                    names: alloc::vec![oid],
10357                });
10358            }
10359            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10360            // argument-list parse as DROP AGGREGATE (round 707); the
10361            // action no-ops, the existence check is real.
10362            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10363                // Same round-695 trap as above: AGGREGATE is already
10364                // consumed; the cursor is at the name.
10365                let name = self.expect_ident_like()?;
10366                let mut names = alloc::vec![name];
10367                if matches!(self.peek(), Token::LParen) {
10368                    self.advance();
10369                    loop {
10370                        match self.peek().clone() {
10371                            Token::RParen => {
10372                                self.advance();
10373                                break;
10374                            }
10375                            Token::Star => {
10376                                self.advance();
10377                                names.push(String::from("*"));
10378                            }
10379                            Token::Comma => {
10380                                self.advance();
10381                            }
10382                            _ => {
10383                                let mut t = self.expect_ident_like()?;
10384                                while let Token::Ident(nx) = self.peek() {
10385                                    let nx = nx.clone();
10386                                    self.advance();
10387                                    t.push(' ');
10388                                    t.push_str(&nx);
10389                                }
10390                                names.push(t);
10391                            }
10392                        }
10393                    }
10394                }
10395                self.consume_until_statement_boundary();
10396                return Ok(Statement::ValidateOnly {
10397                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10398                    names,
10399                });
10400            }
10401            Token::Ident(s) | Token::QuotedIdent(s)
10402                if matches!(
10403                    s.to_ascii_lowercase().as_str(),
10404                    "view"
10405                        | "function"
10406                        | "database"
10407                        | "schema"
10408                        | "owner"
10409                        | "default"
10410                        | "extension"
10411                        | "materialized"
10412                        | "publication"
10413                        | "subscription"
10414                        // v7.37.17 (17.6 siblings) — additional ALTER
10415                        // targets pg_dump / pg_dumpall / operator DB
10416                        // migration scripts commonly emit. SPG has
10417                        // no matching machinery for any of these; the
10418                        // parser accepts + Empty-returns so pg_dump
10419                        // tail statements don't stall.
10420                        | "tablespace"
10421                        | "language"
10422                        | "operator"
10423                        | "conversion"
10424                        | "statistics"
10425                        | "server"
10426                        | "foreign"
10427                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10428                        // / TEMPLATE (CONFIGURATION intercepted above).
10429                        | "text"
10430                ) =>
10431            {
10432                self.consume_until_statement_boundary();
10433                return Ok(Statement::Empty);
10434            }
10435            other => {
10436                return Err(self.err(format!(
10437                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10438                     after ALTER, got {other:?}"
10439                )));
10440            }
10441        }
10442        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10443        // (mailrs migrate-042 ships these). The presence of an
10444        // IF EXISTS makes the subsequent name lookup tolerate
10445        // a missing index — engine returns CommandOk no-op.
10446        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10447            let next = self.tokens.get(self.pos + 1);
10448            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10449                self.advance();
10450                self.advance();
10451                true
10452            } else {
10453                false
10454            }
10455        } else {
10456            false
10457        };
10458        let name = self.expect_ident_like()?;
10459        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10460        // Detect BEFORE the REBUILD path so the existing REBUILD
10461        // arm stays untouched.
10462        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10463            self.advance();
10464            if matches!(self.peek(), Token::To) {
10465                self.advance();
10466            } else {
10467                self.expect_keyword_ident("to")?;
10468            }
10469            let new = self.expect_ident_like()?;
10470            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10471                name,
10472                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10473            }));
10474        }
10475        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10476        // A syntax error before; the index is validated, the params no-op.
10477        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10478            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10479                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10480        {
10481            self.consume_until_statement_boundary();
10482            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10483                name,
10484                target: crate::ast::AlterIndexTarget::StorageParams,
10485            }));
10486        }
10487        // REBUILD
10488        self.expect_keyword_ident("rebuild")?;
10489        // Optional: WITH (encoding = <enc>)
10490        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10491            self.advance();
10492            if !matches!(self.peek(), Token::LParen) {
10493                return Err(self.err(format!(
10494                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10495                    self.peek()
10496                )));
10497            }
10498            self.advance();
10499            self.expect_keyword_ident("encoding")?;
10500            if !matches!(self.peek(), Token::Eq) {
10501                return Err(self.err(format!(
10502                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10503                    self.peek()
10504                )));
10505            }
10506            self.advance();
10507            let enc_ident = match self.advance() {
10508                Token::Ident(s) | Token::QuotedIdent(s) => s,
10509                other => {
10510                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10511                }
10512            };
10513            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10514                "f32" => VecEncoding::F32,
10515                "sq8" => VecEncoding::Sq8,
10516                "half" => VecEncoding::F16,
10517                other => {
10518                    return Err(self.err(format!(
10519                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10520                    )));
10521                }
10522            };
10523            if !matches!(self.peek(), Token::RParen) {
10524                return Err(self.err(format!(
10525                    "expected ')' after encoding value, got {:?}",
10526                    self.peek()
10527                )));
10528            }
10529            self.advance();
10530            Some(enc)
10531        } else {
10532            None
10533        };
10534        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10535            name,
10536            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10537        }))
10538    }
10539
10540    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10541    /// only `SET` form currently supported; future v6.7.x can add
10542    /// more SET subjects without changing the dispatch shape.
10543    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10544    /// subactions. Single-subaction shape stays a 1-element vec.
10545    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10546        let table_name = self.expect_ident_like()?;
10547        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10548        loop {
10549            let subaction = self.parse_alter_table_subaction()?;
10550            // ADD COLUMN with inline REFERENCES emits both an
10551            // AddColumn and an AddForeignKey subaction; the
10552            // helper returns 1 or 2 items.
10553            targets.extend(subaction);
10554            if matches!(self.peek(), Token::Comma) {
10555                self.advance();
10556                continue;
10557            }
10558            break;
10559        }
10560        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10561            name: table_name,
10562            targets,
10563        }))
10564    }
10565
10566    /// Parse one ALTER TABLE subaction. Returns a Vec because
10567    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10568    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10569    fn parse_alter_table_subaction(
10570        &mut self,
10571    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10572        match self.peek() {
10573            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10574                self.advance();
10575                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10576                // storage parameters: paren-prefixed; consume.
10577                if matches!(self.peek(), Token::LParen) {
10578                    self.consume_until_statement_boundary();
10579                    return Ok(Vec::new());
10580                }
10581                let setting = self.expect_ident_like()?;
10582                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10583                    if !matches!(self.peek(), Token::Eq) {
10584                        return Err(self.err(alloc::format!(
10585                            "expected '=' after hot_tier_bytes, got {:?}",
10586                            self.peek()
10587                        )));
10588                    }
10589                    self.advance();
10590                    let n = self.expect_u64_literal()?;
10591                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10592                }
10593                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10594                // accept-and-no-op for ALTER TABLE SET <subject>
10595                // forms that pg_dump emits but SPG either treats
10596                // as N/A (single-tenant, single-owner, no shared
10597                // tablespaces) or accepts the dump-side declaration
10598                // without runtime effect:
10599                //   SET SCHEMA <name>            (18.11)
10600                //   SET TABLESPACE <name>        (18.8)
10601                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10602                //   SET WITHOUT CLUSTER          (18.13)
10603                //   SET WITHOUT OIDS             (PG legacy)
10604                //   SET (option = value, …)      (storage parameters)
10605                //   SET REPLICA IDENTITY {…}     (18.14)
10606                if setting.eq_ignore_ascii_case("schema")
10607                    || setting.eq_ignore_ascii_case("tablespace")
10608                    || setting.eq_ignore_ascii_case("logged")
10609                    || setting.eq_ignore_ascii_case("unlogged")
10610                    || setting.eq_ignore_ascii_case("without")
10611                {
10612                    self.consume_until_statement_boundary();
10613                    return Ok(Vec::new());
10614                }
10615                if setting.eq_ignore_ascii_case("replica") {
10616                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10617                    self.consume_until_statement_boundary();
10618                    return Ok(Vec::new());
10619                }
10620                // SET (option=value, …) — storage parameters.
10621                if matches!(self.peek(), Token::LParen) {
10622                    self.consume_until_statement_boundary();
10623                    return Ok(Vec::new());
10624                }
10625                Err(self.err(alloc::format!(
10626                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10627                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10628                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10629                )))
10630            }
10631            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10632            // not ignored: round 645 gave SPG the inheritance the
10633            // v7.37.18 no-op said it did not have.
10634            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10635                self.advance();
10636                let parent = self.expect_ident_like()?;
10637                self.consume_until_statement_boundary();
10638                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10639                    parent,
10640                    detach: false
10641                }])
10642            }
10643            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10644            // LEVEL SECURITY`, which has its own RLS arm below — without
10645            // the guard this swallowed NO FORCE as a no-op.
10646            Token::Ident(s)
10647                if s.eq_ignore_ascii_case("no")
10648                    && !matches!(
10649                        self.tokens.get(self.pos + 1),
10650                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10651                    ) =>
10652            {
10653                self.advance();
10654                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10655                    if k.eq_ignore_ascii_case("inherit"))
10656                {
10657                    self.advance();
10658                    let parent = self.expect_ident_like()?;
10659                    self.consume_until_statement_boundary();
10660                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10661                        parent,
10662                        detach: true
10663                    }]);
10664                }
10665                self.consume_until_statement_boundary();
10666                Ok(Vec::new())
10667            }
10668            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10669            // single-owner, so there is still nothing to record.
10670            //
10671            // v7.39 (round 652) — but the name now reaches the engine,
10672            // which refuses a role that does not exist as PG does. The
10673            // no-op was swallowing the whole statement, so a dump naming
10674            // a role this server never heard of restored clean and left
10675            // the table owned by whoever ran the restore.
10676            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10677                self.advance();
10678                if matches!(self.peek(), Token::To) {
10679                    self.advance();
10680                }
10681                let role = self.expect_ident_like()?;
10682                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10683                    role
10684                }])
10685            }
10686            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10687            // PG sets a hint; SPG doesn't have clustered storage, so the
10688            // hint itself stays a no-op.
10689            //
10690            // v7.39 (round 652) — the index name is checked now. PG
10691            // errors on one that does not exist, and swallowing that let
10692            // a typo'd CLUSTER ON pass silently.
10693            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10694                self.advance();
10695                // `ON` is a reserved token, not an ident.
10696                if !matches!(self.peek(), Token::On) {
10697                    return Err(self.err(alloc::format!(
10698                        "expected ON after CLUSTER, got {:?}",
10699                        self.peek()
10700                    )));
10701                }
10702                self.advance();
10703                let index = self.expect_ident_like()?;
10704                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10705                    index: Some(index)
10706                }])
10707            }
10708            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10709            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10710            // what a logical decoder puts in the old-tuple image; SPG's
10711            // replication is SQL-text, so there is nothing to record.
10712            // Accept-and-no-op (it used to be a parse error).
10713            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10714                self.advance();
10715                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10716                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10717                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10718                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10719                {
10720                    self.advance(); // IDENTITY
10721                    self.advance(); // USING
10722                    if matches!(self.peek(), Token::Index)
10723                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10724                    {
10725                        self.advance();
10726                    }
10727                    let index = self.expect_ident_like()?;
10728                    self.consume_until_statement_boundary();
10729                    return Ok(alloc::vec![
10730                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10731                    ]);
10732                }
10733                self.consume_until_statement_boundary();
10734                Ok(Vec::new())
10735            }
10736            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10737            //
10738            // v7.39 (round 652) — it used to consume the statement and
10739            // return nothing, on the stated theory that SPG validated at
10740            // ADD CONSTRAINT time so there was never anything left to
10741            // validate. Measured against PG18, ADD CONSTRAINT did not
10742            // scan the existing rows at all — the comment described a
10743            // property SPG did not have, which is why nobody looked. Both
10744            // halves are real now: ADD scans unless told NOT VALID, and
10745            // this scans what NOT VALID skipped.
10746            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10747                self.advance();
10748                self.expect_keyword_ident("constraint")?;
10749                let name = self.expect_ident_like()?;
10750                Ok(alloc::vec![
10751                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10752                ])
10753            }
10754            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10755            // SET (option = value, …). PG uses it to clear per-table
10756            // storage params like fillfactor or autovacuum_*. SPG
10757            // engine-manages those parameters; accept-and-no-op.
10758            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10759                self.advance();
10760                self.consume_until_statement_boundary();
10761                Ok(Vec::new())
10762            }
10763            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10764            // type-of binding (PG 9.0+). SPG composite types
10765            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10766            // TABLE OF is rare and inverse of CREATE TABLE OF.
10767            // Accept-and-no-op until a customer dump round-trips it.
10768            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10769                self.advance();
10770                // v7.39 (round 710) — the type name is validated now.
10771                let type_name = self.expect_ident_like()?;
10772                self.consume_until_statement_boundary();
10773                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10774                    type_name
10775                }])
10776            }
10777            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10778            // (reserved keyword) rather than Token::Ident("not"),
10779            // so it needs its own arm. Accept-and-no-op same as OF.
10780            Token::Not => {
10781                self.advance();
10782                self.consume_until_statement_boundary();
10783                Ok(Vec::new())
10784            }
10785            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10786            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10787                self.advance();
10788                self.expect_row_level_security()?;
10789                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10790                    enabled: None,
10791                    force: Some(true),
10792                }])
10793            }
10794            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10795            Token::Ident(s)
10796                if s.eq_ignore_ascii_case("no")
10797                    && matches!(
10798                        self.tokens.get(self.pos + 1),
10799                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10800                    ) =>
10801            {
10802                self.advance(); // NO
10803                self.advance(); // FORCE
10804                self.expect_row_level_security()?;
10805                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10806                    enabled: None,
10807                    force: Some(false),
10808                }])
10809            }
10810            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10811            // (sets relrowsecurity). The guard requires the next token to be
10812            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10813            Token::Ident(s)
10814                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10815                    && matches!(
10816                        self.tokens.get(self.pos + 1),
10817                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10818                    ) =>
10819            {
10820                let enabled = s.eq_ignore_ascii_case("enable");
10821                self.advance(); // ENABLE/DISABLE
10822                self.expect_row_level_security()?;
10823                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10824                    enabled: Some(enabled),
10825                    force: None,
10826                }])
10827            }
10828            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10829                self.advance();
10830                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10831                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10832                // emits. The same grammar CREATE TABLE already accepts
10833                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10834                // through the SAME parser — an ALTER-only copy would be a
10835                // second place for the two to drift.
10836                if self.peek_mysql_inline_key_start() {
10837                    return Ok(match self.parse_mysql_inline_key()? {
10838                        Some(c) => {
10839                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10840                        }
10841                        // FULLTEXT / SPATIAL parse and are accepted as a
10842                        // no-op here exactly as they are inline.
10843                        None => Vec::new(),
10844                    });
10845                }
10846                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10847                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10848                // PRIMARY KEY this way; mysqldump emits both.
10849                // Peek-only dispatch (no advance) — `advance()`
10850                // destructively replaces consumed tokens with Eof,
10851                // so saved-pos restore would land on Eofs.
10852                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10853                {
10854                    // The next-but-one ident is the constraint
10855                    // name; the one after THAT is the kind.
10856                    let kind_pos = self.pos + 2;
10857                    let kind = self.tokens.get(kind_pos).cloned();
10858                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10859                    {
10860                        let fk = self.parse_table_level_fk()?;
10861                        return Ok(alloc::vec![
10862                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10863                        ]);
10864                    }
10865                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10866                    {
10867                        self.advance(); // CONSTRAINT
10868                        // v7.39 (read01 round 48) — keep the name; the engine
10869                        // stores it now instead of dropping it on the floor.
10870                        let con_name = self.expect_ident_like()?;
10871                        self.advance(); // PRIMARY
10872                        self.expect_keyword_ident("key")?;
10873                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10874                        // v7.39 (round 711) — the ALTER form carries the
10875                        // timing too (pg_dump writes it here).
10876                        let (deferrable, initially_deferred) =
10877                            self.consume_deferrable_clauses_timed()?;
10878                        return Ok(alloc::vec![
10879                            crate::ast::AlterTableTarget::AddTableConstraint(
10880                                crate::ast::TableConstraint::PrimaryKey {
10881                                    name: Some(con_name),
10882                                    columns: cols,
10883                                    deferrable,
10884                                    initially_deferred,
10885                                }
10886                            )
10887                        ]);
10888                    }
10889                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10890                    {
10891                        self.advance(); // CONSTRAINT
10892                        // v7.39 (read01 round 48) — keep the name.
10893                        let con_name = self.expect_ident_like()?;
10894                        // v7.22 (mailrs round-13 gap 6) — delegate so
10895                        // the optional `NULLS [NOT] DISTINCT` modifier
10896                        // parses here too (pg_dump emits the ALTER
10897                        // form; semantics enforced by the engine
10898                        // since v7.13).
10899                        let mut uc = self.parse_table_level_unique()?;
10900                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10901                            *name = Some(con_name);
10902                        }
10903                        return Ok(alloc::vec![
10904                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10905                        ]);
10906                    }
10907                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10908                    {
10909                        self.advance(); // CONSTRAINT
10910                        // v7.39 (read01 round 48) — keep the name.
10911                        let con_name = self.expect_ident_like()?;
10912                        self.advance(); // CHECK
10913                        if !matches!(self.peek(), Token::LParen) {
10914                            return Err(self.err(alloc::format!(
10915                                "expected '(' after CHECK, got {:?}", self.peek()
10916                            )));
10917                        }
10918                        self.advance();
10919                        let expr = self.parse_expr(0)?;
10920                        if matches!(self.peek(), Token::RParen) {
10921                            self.advance();
10922                        }
10923                        let not_valid = self.parse_not_valid_suffix();
10924                        return Ok(alloc::vec![
10925                            crate::ast::AlterTableTarget::AddTableConstraint(
10926                                crate::ast::TableConstraint::Check {
10927                                    name: Some(con_name),
10928                                    expr,
10929                                    not_valid,
10930                                }
10931                            )
10932                        ]);
10933                    }
10934                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10935                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10936                    // exclusion constraints via this ALTER form.
10937                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10938                    {
10939                        self.advance(); // CONSTRAINT
10940                        let con_name = self.expect_ident_like()?;
10941                        let mut ex = self.parse_table_level_exclude()?;
10942                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10943                            *name = Some(con_name);
10944                        }
10945                        return Ok(alloc::vec![
10946                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10947                        ]);
10948                    }
10949                    // Unknown kind — fall through to FK path which
10950                    // produces a descriptive parse error.
10951                }
10952                let is_fk = matches!(
10953                    self.peek(),
10954                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10955                        || s.eq_ignore_ascii_case("foreign")
10956                );
10957                if is_fk {
10958                    let fk = self.parse_table_level_fk()?;
10959                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10960                }
10961                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10962                // (no CONSTRAINT prefix) — same dispatch.
10963                match self.peek().clone() {
10964                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10965                        self.advance();
10966                        self.expect_keyword_ident("key")?;
10967                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10968                        let (deferrable, initially_deferred) =
10969                            self.consume_deferrable_clauses_timed()?;
10970                        return Ok(alloc::vec![
10971                            crate::ast::AlterTableTarget::AddTableConstraint(
10972                                crate::ast::TableConstraint::PrimaryKey {
10973                                    name: None,
10974                                    columns: cols,
10975                                    deferrable,
10976                                    initially_deferred,
10977                                }
10978                            )
10979                        ]);
10980                    }
10981                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10982                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
10983                        let uc = self.parse_table_level_unique()?;
10984                        return Ok(alloc::vec![
10985                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10986                        ]);
10987                    }
10988                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10989                    // prefix). The other three bare forms were here and
10990                    // this one was not, so it fell through to the column
10991                    // path and came back as "unexpected reserved keyword
10992                    // 'check' at start of column definition".
10993                    _ if self.peek_table_level_check_start() => {
10994                        let chk = self.parse_table_level_check()?;
10995                        let not_valid = self.parse_not_valid_suffix();
10996                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10997                            unreachable!("parse_table_level_check returns Check")
10998                        };
10999                        return Ok(alloc::vec![
11000                            crate::ast::AlterTableTarget::AddTableConstraint(
11001                                crate::ast::TableConstraint::Check {
11002                                    name: None,
11003                                    expr,
11004                                    not_valid,
11005                                }
11006                            )
11007                        ]);
11008                    }
11009                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11010                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11011                        let ex = self.parse_table_level_exclude()?;
11012                        return Ok(alloc::vec![
11013                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11014                        ]);
11015                    }
11016                    _ => {}
11017                }
11018                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11019                    self.advance();
11020                }
11021                let mut if_not_exists = false;
11022                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11023                    self.advance();
11024                    if !matches!(self.peek(), Token::Not) {
11025                        return Err(self.err(alloc::format!(
11026                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11027                            self.peek()
11028                        )));
11029                    }
11030                    self.advance();
11031                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11032                        return Err(self.err(alloc::format!(
11033                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11034                            self.peek()
11035                        )));
11036                    }
11037                    self.advance();
11038                    if_not_exists = true;
11039                }
11040                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11041                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11042                // returns ColumnDef + an optional inline FK.
11043                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11044                let col_name = column.name.clone();
11045                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11046                    column,
11047                    if_not_exists,
11048                }];
11049                if let Some(mut fk) = col_level_fk {
11050                    if fk.columns.is_empty() {
11051                        fk.columns.push(col_name);
11052                    }
11053                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11054                }
11055                Ok(out)
11056            }
11057            Token::Drop => {
11058                self.advance();
11059                // v7.13.3 — dispatch on the next token. mailrs round-7
11060                // S8 closed DROP COLUMN; round-6 S7 closed
11061                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11062                // RESTRICT modifiers.
11063                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11064                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11065                let subject = match self.peek() {
11066                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11067                        self.advance();
11068                        "constraint"
11069                    }
11070                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11071                        self.advance();
11072                        "column"
11073                    }
11074                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11075                    // `INDEX` lexes as the reserved Token::Index, so it is
11076                    // unambiguous. `KEY` is a plain ident, and PG allows a
11077                    // column literally named "key", so only read it as the
11078                    // keyword when a name follows it.
11079                    Token::Index => {
11080                        self.advance();
11081                        "index"
11082                    }
11083                    Token::Ident(s)
11084                        if s.eq_ignore_ascii_case("key")
11085                            && matches!(
11086                                self.tokens.get(self.pos + 1),
11087                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11088                            ) =>
11089                    {
11090                        self.advance();
11091                        "index"
11092                    }
11093                    // PG-canonical bare `DROP <col>` without COLUMN
11094                    // keyword is also valid; treat any other ident
11095                    // as the column name.
11096                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11097                    other => {
11098                        return Err(self.err(alloc::format!(
11099                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11100                        )));
11101                    }
11102                };
11103                let mut if_exists = false;
11104                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11105                    let n1 = self.tokens.get(self.pos + 1);
11106                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11107                        self.advance();
11108                        self.advance();
11109                        if_exists = true;
11110                    }
11111                }
11112                let name = self.expect_ident_like()?;
11113                let mut cascade = false;
11114                if matches!(
11115                    self.peek(),
11116                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11117                        || s.eq_ignore_ascii_case("restrict")
11118                ) {
11119                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11120                    {
11121                        cascade = true;
11122                    }
11123                    self.advance();
11124                }
11125                if subject == "index" {
11126                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11127                        name,
11128                        if_exists,
11129                    }])
11130                } else if subject == "constraint" {
11131                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11132                        name,
11133                        if_exists,
11134                    }])
11135                } else {
11136                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11137                        column: name,
11138                        if_exists,
11139                        cascade,
11140                    }])
11141                }
11142            }
11143            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11144                self.advance();
11145                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11146                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11147                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11148                // immediately; accept-and-no-op.
11149                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11150                    self.advance();
11151                    self.consume_until_statement_boundary();
11152                    return Ok(Vec::new());
11153                }
11154                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11155                    self.advance();
11156                }
11157                let col_name = self.expect_ident_like()?;
11158                match self.peek() {
11159                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11160                        self.advance();
11161                    }
11162                    // v7.14.0 — pg_dump emits BIGSERIAL via
11163                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11164                    // nextval('seq')` (the sequence is created
11165                    // separately). SPG's BIGSERIAL already uses
11166                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11167                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11168                    // engine no-ops by consuming the tail.
11169                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11170                        // v7.22 (round-13 T2) — `SET DEFAULT
11171                        // nextval('…')` is how pg_dump spells a
11172                        // SERIAL column (plain integer in CREATE
11173                        // TABLE + this ALTER). It used to be
11174                        // swallowed as a no-op, which silently
11175                        // STRIPPED auto-increment from imported
11176                        // schemas — the first post-import INSERT
11177                        // without an explicit id then violated NOT
11178                        // NULL. Lower it to the auto-increment
11179                        // marker instead.
11180                        let is_default_nextval =
11181                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11182                                && matches!(
11183                                    self.tokens.get(self.pos + 2),
11184                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11185                                );
11186                        if is_default_nextval {
11187                            let seq_name = self.scan_sequence_name_until_boundary();
11188                            return Ok(alloc::vec![
11189                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11190                                    column: col_name,
11191                                    seq_name,
11192                                }
11193                            ]);
11194                        }
11195                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11196                        self.advance(); // consume "set"
11197                        match self.peek().clone() {
11198                            Token::Default => {
11199                                self.advance();
11200                                let default_expr = self.parse_expr(0)?;
11201                                return Ok(alloc::vec![
11202                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11203                                        column: col_name,
11204                                        default_expr,
11205                                    }
11206                                ]);
11207                            }
11208                            Token::Not => {
11209                                self.advance();
11210                                if !matches!(self.peek(), Token::Null) {
11211                                    return Err(self.err(alloc::format!(
11212                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11213                                        self.peek()
11214                                    )));
11215                                }
11216                                self.advance();
11217                                return Ok(alloc::vec![
11218                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11219                                        column: col_name,
11220                                    }
11221                                ]);
11222                            }
11223                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11224                            // stored generated column's expression and
11225                            // recompute existing rows.
11226                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11227                                self.advance(); // EXPRESSION
11228                                if matches!(self.peek(), Token::As) {
11229                                    self.advance();
11230                                }
11231                                let expr = self.parse_expr(0)?;
11232                                return Ok(alloc::vec![
11233                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11234                                        column: col_name,
11235                                        expr,
11236                                    }
11237                                ]);
11238                            }
11239                            other => {
11240                                // Other SET subjects (STATISTICS,
11241                                // STORAGE, COMPRESSION, …) stay no-ops —
11242                                // storage hints with no SPG semantics.
11243                                let _ = other;
11244                                self.consume_until_statement_boundary();
11245                                return Ok(Vec::new());
11246                            }
11247                        }
11248                    }
11249                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11250                        self.advance(); // consume "drop"
11251                        return self.parse_alter_column_drop_tail(col_name);
11252                    }
11253                    Token::Drop => {
11254                        self.advance(); // consume Drop token
11255                        return self.parse_alter_column_drop_tail(col_name);
11256                    }
11257                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11258                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11259                        // GENERATED { ALWAYS | BY DEFAULT } AS
11260                        // IDENTITY ( … )`: pg_dump's spelling for
11261                        // identity columns. Same auto-increment
11262                        // lowering as the nextval default; the
11263                        // sequence options inside the parens are
11264                        // no-ops under SPG's max+1 semantics.
11265                        let is_generated = matches!(
11266                            self.tokens.get(self.pos + 1),
11267                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11268                        );
11269                        if !is_generated {
11270                            return Err(self.err(alloc::format!(
11271                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11272                                self.tokens.get(self.pos + 1)
11273                            )));
11274                        }
11275                        let seq_name = self.scan_sequence_name_until_boundary();
11276                        return Ok(alloc::vec![
11277                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11278                                column: col_name,
11279                                seq_name,
11280                            }
11281                        ]);
11282                    }
11283                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11284                    // column: floor the next allocated value at n (bare
11285                    // RESTART = restart from the start value, 1).
11286                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11287                        self.advance();
11288                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11289                        {
11290                            self.advance();
11291                            let neg = if matches!(self.peek(), Token::Minus) {
11292                                self.advance();
11293                                true
11294                            } else {
11295                                false
11296                            };
11297                            match self.advance() {
11298                                Token::Integer(v) => Some(if neg { -v } else { v }),
11299                                other => {
11300                                    return Err(self.err(alloc::format!(
11301                                        "expected integer after RESTART WITH, got {other:?}"
11302                                    )));
11303                                }
11304                            }
11305                        } else {
11306                            None
11307                        };
11308                        return Ok(alloc::vec![
11309                            crate::ast::AlterTableTarget::AlterColumnRestart {
11310                                column: col_name,
11311                                with,
11312                            }
11313                        ]);
11314                    }
11315                    other => {
11316                        return Err(self.err(alloc::format!(
11317                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11318                        )));
11319                    }
11320                }
11321                // v7.39 (round 713) — the type parser has consumed a
11322                // trailing `COLLATE <name>` since Phase 2.5, and
11323                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11324                // TYPE text COLLATE "C"` parsed clean and changed
11325                // nothing. Keep the clause; the engine re-collates.
11326                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11327                    self.parse_type_with_implied_flags()?;
11328                let collation = if coll_explicit {
11329                    coll_name.map(|n| (coll, n))
11330                } else {
11331                    None
11332                };
11333                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11334                {
11335                    self.advance();
11336                    Some(self.parse_expr(0)?)
11337                } else {
11338                    None
11339                };
11340                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11341                    column: col_name,
11342                    new_type,
11343                    using,
11344                    collation,
11345                }])
11346            }
11347            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11348            // PG also supports `RENAME TO new_table` for table-name
11349            // rename; that surface is deferred (pg_dump never emits
11350            // it). If the first post-RENAME ident is `TO`, the user
11351            // is asking for table rename — error with a clear
11352            // message rather than misparsing `TO` as a column name.
11353            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11354                self.advance();
11355                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11356                // table-name rename (mailrs round-10 A.5 — used
11357                // by migrate-042's `RENAME TO email_contacts`).
11358                // `TO` lexes as Token::To.
11359                if matches!(self.peek(), Token::To)
11360                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11361                {
11362                    self.advance();
11363                    let new = self.expect_ident_like()?;
11364                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11365                        new,
11366                    }]);
11367                }
11368                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11369                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11370                    self.advance();
11371                    let old = self.expect_ident_like()?;
11372                    if matches!(self.peek(), Token::To) {
11373                        self.advance();
11374                    } else {
11375                        self.expect_keyword_ident("to")?;
11376                    }
11377                    let new = self.expect_ident_like()?;
11378                    return Ok(alloc::vec![
11379                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11380                    ]);
11381                }
11382                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11383                    self.advance();
11384                }
11385                let old = self.expect_ident_like()?;
11386                // `TO` is a reserved keyword token; accept both
11387                // Token::To and Token::Ident("to") for consistency.
11388                if matches!(self.peek(), Token::To) {
11389                    self.advance();
11390                } else {
11391                    self.expect_keyword_ident("to")?;
11392                }
11393                let new = self.expect_ident_like()?;
11394                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11395                    old,
11396                    new,
11397                }])
11398            }
11399            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11400            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11401            // every data block with these. Real disable semantics —
11402            // not no-op — because reload correctness assumes the
11403            // triggers don't fire (rows already carry their
11404            // computed values from prod).
11405            Token::Ident(s)
11406                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11407            {
11408                let enabled = s.eq_ignore_ascii_case("enable");
11409                self.advance();
11410                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11411                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11412                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11413                // pg_dump output) — anything else falls through to
11414                // the catch-all error below.
11415                // v7.22 (round-13 T3) — mysqldump wraps every data
11416                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11417                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11418                // maintains indexes incrementally — engine no-op.
11419                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11420                    self.advance();
11421                    return Ok(Vec::new());
11422                }
11423                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11424                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11425                // to gate triggers on session_replication_role; SPG
11426                // has no replica role, so the prefix is consumed and
11427                // treated identically to the plain ENABLE/DISABLE
11428                // TRIGGER form.
11429                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11430                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11431                {
11432                    self.advance();
11433                }
11434                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11435                    return Err(self.err(alloc::format!(
11436                        "expected TRIGGER after {}, got {:?}",
11437                        if enabled { "ENABLE" } else { "DISABLE" },
11438                        self.peek()
11439                    )));
11440                }
11441                self.advance();
11442                // `ALL` lexes as Token::All (reserved); also
11443                // accept Token::Ident("all") for symmetry.
11444                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11445                // TRIGGER selectors. USER (= all user triggers) is
11446                // semantically ALL here; REPLICA / ALWAYS gate on
11447                // session_replication_role which SPG doesn't track.
11448                // All map to TriggerSelector::All.
11449                let which = if matches!(self.peek(), Token::All)
11450                    || matches!(self.peek(), Token::Ident(s)
11451                        if s.eq_ignore_ascii_case("all")
11452                            || s.eq_ignore_ascii_case("user")
11453                            || s.eq_ignore_ascii_case("replica")
11454                            || s.eq_ignore_ascii_case("always"))
11455                {
11456                    self.advance();
11457                    crate::ast::TriggerSelector::All
11458                } else {
11459                    let name = self.expect_ident_like()?;
11460                    crate::ast::TriggerSelector::Named(name)
11461                };
11462                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11463                    which,
11464                    enabled,
11465                }])
11466            }
11467            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11468            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11469                self.advance();
11470                if !matches!(self.peek(), Token::Partition)
11471                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11472                        if s.eq_ignore_ascii_case("partition"))
11473                {
11474                    return Err(self.err(alloc::format!(
11475                        "expected PARTITION after ATTACH, got {:?}",
11476                        self.peek()
11477                    )));
11478                }
11479                self.advance();
11480                let child = self.expect_ident_like()?;
11481                let bounds = self.parse_partition_bounds_tail()?;
11482                Ok(alloc::vec![
11483                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11484                ])
11485            }
11486            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11487            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11488                self.advance();
11489                if !matches!(self.peek(), Token::Partition)
11490                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11491                        if s.eq_ignore_ascii_case("partition"))
11492                {
11493                    return Err(self.err(alloc::format!(
11494                        "expected PARTITION after DETACH, got {:?}",
11495                        self.peek()
11496                    )));
11497                }
11498                self.advance();
11499                let child = self.expect_ident_like()?;
11500                let mut concurrently = false;
11501                let mut finalize = false;
11502                loop {
11503                    match self.peek().clone() {
11504                        Token::Ident(s) | Token::QuotedIdent(s)
11505                            if s.eq_ignore_ascii_case("concurrently") =>
11506                        {
11507                            self.advance();
11508                            concurrently = true;
11509                        }
11510                        Token::Ident(s) | Token::QuotedIdent(s)
11511                            if s.eq_ignore_ascii_case("finalize") =>
11512                        {
11513                            self.advance();
11514                            finalize = true;
11515                        }
11516                        _ => break,
11517                    }
11518                }
11519                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11520                    child,
11521                    concurrently,
11522                    finalize,
11523                }])
11524            }
11525            other => Err(self.err(alloc::format!(
11526                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11527            ))),
11528        }
11529    }
11530
11531    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11532    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11533    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11534    /// `parse_partition_of_tail`'s bounds branch.
11535    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11536    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11537    /// lowering each to the respective AlterTableTarget. Any
11538    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11539    /// no-op via consume_until_statement_boundary.
11540    fn parse_alter_column_drop_tail(
11541        &mut self,
11542        col_name: String,
11543    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11544        match self.peek().clone() {
11545            Token::Default => {
11546                self.advance();
11547                Ok(alloc::vec![
11548                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11549                ])
11550            }
11551            Token::Not => {
11552                self.advance();
11553                if !matches!(self.peek(), Token::Null) {
11554                    return Err(self.err(alloc::format!(
11555                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11556                        self.peek()
11557                    )));
11558                }
11559                self.advance();
11560                Ok(alloc::vec![
11561                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11562                ])
11563            }
11564            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11565            // generated column into a plain column.
11566            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11567                self.advance();
11568                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11569                // dropped, so the engine still errored on a plain
11570                // column; PG's semantics are NOTICE + skip.
11571                let mut if_exists = false;
11572                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11573                    self.advance();
11574                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11575                        self.advance();
11576                        if_exists = true;
11577                    }
11578                }
11579                Ok(alloc::vec![
11580                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11581                        column: col_name,
11582                        if_exists,
11583                    }
11584                ])
11585            }
11586            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11587            // identity column into a plain column.
11588            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11589                self.advance();
11590                let mut if_exists = false;
11591                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11592                    self.advance();
11593                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11594                        self.advance();
11595                        if_exists = true;
11596                    }
11597                }
11598                Ok(alloc::vec![
11599                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11600                        column: col_name,
11601                        if_exists,
11602                    }
11603                ])
11604            }
11605            _ => {
11606                self.consume_until_statement_boundary();
11607                Ok(Vec::new())
11608            }
11609        }
11610    }
11611
11612    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11613    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11614    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11615    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11616    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11617        let mut opts = crate::ast::CopyOptions::default();
11618        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11619            return Ok(opts);
11620        }
11621        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11622            self.advance();
11623        }
11624        if matches!(self.peek(), Token::LParen) {
11625            self.advance();
11626            loop {
11627                self.parse_one_copy_option(&mut opts)?;
11628                match self.peek() {
11629                    Token::Comma => {
11630                        self.advance();
11631                    }
11632                    Token::RParen => {
11633                        self.advance();
11634                        break;
11635                    }
11636                    other => {
11637                        return Err(self.err(alloc::format!(
11638                            "expected ',' or ')' in COPY options, got {other:?}"
11639                        )));
11640                    }
11641                }
11642            }
11643        } else {
11644            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11645                self.parse_one_copy_option(&mut opts)?;
11646            }
11647        }
11648        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11649            return Err(self.err(alloc::format!(
11650                "unexpected token after COPY options: {:?}",
11651                self.peek()
11652            )));
11653        }
11654        Ok(opts)
11655    }
11656
11657    fn parse_one_copy_option(
11658        &mut self,
11659        opts: &mut crate::ast::CopyOptions,
11660    ) -> Result<(), ParseError> {
11661        use crate::ast::CopyFormat;
11662        // The option keyword. NULL lexes as its own token; the rest are
11663        // bare identifiers.
11664        let kw = match self.advance() {
11665            Token::Null => alloc::string::String::from("NULL"),
11666            Token::Ident(s) => s.to_uppercase(),
11667            other => {
11668                return Err(self.err(alloc::format!(
11669                    "expected a COPY option keyword, got {other:?}"
11670                )));
11671            }
11672        };
11673        match kw.as_str() {
11674            "FORMAT" => {
11675                let fmt = self.expect_ident_like()?;
11676                match fmt.to_ascii_uppercase().as_str() {
11677                    "CSV" => opts.format = CopyFormat::Csv,
11678                    "TEXT" => opts.format = CopyFormat::Text,
11679                    other => {
11680                        return Err(self.err(alloc::format!(
11681                            "COPY format \"{}\" not recognized",
11682                            other.to_ascii_lowercase()
11683                        )));
11684                    }
11685                }
11686            }
11687            // Legacy bare format keywords.
11688            "CSV" => opts.format = CopyFormat::Csv,
11689            "TEXT" => opts.format = CopyFormat::Text,
11690            "HEADER" => {
11691                opts.header = match self.peek() {
11692                    Token::True => {
11693                        self.advance();
11694                        true
11695                    }
11696                    Token::False => {
11697                        self.advance();
11698                        false
11699                    }
11700                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11701                        self.advance();
11702                        true
11703                    }
11704                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11705                        self.advance();
11706                        false
11707                    }
11708                    // Bare HEADER (no boolean) means HEADER true.
11709                    _ => true,
11710                };
11711            }
11712            // r1066 (7.38 S5.1) — pgbench 14+ loads with
11713            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11714            // vacuum bookkeeping on a freshly created/truncated
11715            // table; SPG's per-statement visibility makes it a
11716            // faithful no-op, and rejecting it aborted `pgbench -i`
11717            // against the drop-in. Accept ON/OFF/bare, change nothing.
11718            "FREEZE" => match self.peek() {
11719                Token::True | Token::False => {
11720                    self.advance();
11721                }
11722                Token::Ident(s)
11723                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11724                {
11725                    self.advance();
11726                }
11727                _ => {}
11728            },
11729            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11730                let s = match self.advance() {
11731                    Token::String(s) => s,
11732                    other => {
11733                        return Err(self.err(alloc::format!(
11734                            "COPY {kw} expects a single-character string, got {other:?}"
11735                        )));
11736                    }
11737                };
11738                // v7.39 (round 247) — PG's wording (0A000), keyword in
11739                // lowercase: "COPY delimiter must be a single one-byte
11740                // character".
11741                let one_byte_err = || {
11742                    self.err(alloc::format!(
11743                        "COPY {} must be a single one-byte character",
11744                        kw.to_ascii_lowercase()
11745                    ))
11746                };
11747                let mut chars = s.chars();
11748                let c = chars.next().ok_or_else(one_byte_err)?;
11749                if chars.next().is_some() || c.len_utf8() != 1 {
11750                    return Err(one_byte_err());
11751                }
11752                match kw.as_str() {
11753                    "DELIMITER" => opts.delimiter = Some(c),
11754                    "QUOTE" => opts.quote = Some(c),
11755                    _ => opts.escape = Some(c),
11756                }
11757            }
11758            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11759            "FORCE_QUOTE" => {
11760                if matches!(self.peek(), Token::Star) {
11761                    self.advance();
11762                    opts.force_quote = Some(Vec::new());
11763                } else {
11764                    if !matches!(self.peek(), Token::LParen) {
11765                        return Err(self.err(alloc::format!(
11766                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11767                            self.peek()
11768                        )));
11769                    }
11770                    self.advance();
11771                    let mut cols = Vec::new();
11772                    loop {
11773                        cols.push(self.expect_ident_like()?);
11774                        match self.peek() {
11775                            Token::Comma => {
11776                                self.advance();
11777                            }
11778                            Token::RParen => {
11779                                self.advance();
11780                                break;
11781                            }
11782                            other => {
11783                                return Err(self.err(alloc::format!(
11784                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11785                                )));
11786                            }
11787                        }
11788                    }
11789                    opts.force_quote = Some(cols);
11790                }
11791            }
11792            "NULL" => {
11793                opts.null_str = Some(match self.advance() {
11794                    Token::String(s) => s,
11795                    other => {
11796                        return Err(self.err(alloc::format!(
11797                            "COPY NULL expects a quoted string, got {other:?}"
11798                        )));
11799                    }
11800                });
11801            }
11802            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11803            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11804            // FORCE_NULL too.
11805            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11806                let cols = self.parse_copy_column_list(&kw)?;
11807                if kw == "FORCE_NOT_NULL" {
11808                    opts.force_not_null = Some(cols);
11809                } else {
11810                    opts.force_null = Some(cols);
11811                }
11812            }
11813            other => {
11814                // PG's wording, lowercased option name.
11815                return Err(self.err(alloc::format!(
11816                    "option \"{}\" not recognized",
11817                    other.to_ascii_lowercase()
11818                )));
11819            }
11820        }
11821        Ok(())
11822    }
11823
11824    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11825    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11826    /// is the `*` spelling.
11827    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11828        if matches!(self.peek(), Token::Star) {
11829            self.advance();
11830            return Ok(Vec::new());
11831        }
11832        if !matches!(self.peek(), Token::LParen) {
11833            return Err(self.err(alloc::format!(
11834                "expected '(' or '*' after {kw}, got {:?}",
11835                self.peek()
11836            )));
11837        }
11838        self.advance();
11839        let mut cols = Vec::new();
11840        loop {
11841            cols.push(self.expect_ident_like()?);
11842            match self.peek() {
11843                Token::Comma => {
11844                    self.advance();
11845                }
11846                Token::RParen => {
11847                    self.advance();
11848                    break;
11849                }
11850                other => {
11851                    return Err(self.err(alloc::format!(
11852                        "expected ',' or ')' in {kw} list, got {other:?}"
11853                    )));
11854                }
11855            }
11856        }
11857        Ok(cols)
11858    }
11859
11860    fn parse_partition_bounds_tail(
11861        &mut self,
11862    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11863        use crate::ast::PartitionOfBoundsAst;
11864        match self.peek() {
11865            Token::Default => {
11866                self.advance();
11867                Ok(PartitionOfBoundsAst::Default)
11868            }
11869            Token::For => {
11870                self.advance();
11871                if !matches!(self.peek(), Token::Values) {
11872                    return Err(
11873                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11874                    );
11875                }
11876                self.advance();
11877                let want_with = matches!(
11878                    self.peek(),
11879                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11880                );
11881                if want_with {
11882                    self.advance();
11883                    if !matches!(self.peek(), Token::LParen) {
11884                        return Err(self.err(format!(
11885                            "expected '(' after FOR VALUES WITH, got {:?}",
11886                            self.peek()
11887                        )));
11888                    }
11889                    self.advance();
11890                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11891                    loop {
11892                        let key = self.expect_ident_like()?;
11893                        let n = match self.peek().clone() {
11894                            Token::Integer(v) if u32::try_from(v).is_ok() => {
11895                                self.advance();
11896                                v as u32
11897                            }
11898                            other => {
11899                                return Err(self.err(format!(
11900                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11901                                )));
11902                            }
11903                        };
11904                        match key.to_ascii_uppercase().as_str() {
11905                            "MODULUS" => modulus = Some(n),
11906                            "REMAINDER" => remainder = Some(n),
11907                            other => {
11908                                return Err(self.err(format!(
11909                                    "FOR VALUES WITH: unknown key {other:?}; \
11910                                     expected MODULUS or REMAINDER"
11911                                )));
11912                            }
11913                        }
11914                        match self.peek() {
11915                            Token::Comma => {
11916                                self.advance();
11917                            }
11918                            Token::RParen => {
11919                                self.advance();
11920                                break;
11921                            }
11922                            other => {
11923                                return Err(self.err(format!(
11924                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11925                                )));
11926                            }
11927                        }
11928                    }
11929                    let modulus = modulus
11930                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11931                    let remainder = remainder.ok_or_else(|| {
11932                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11933                    })?;
11934                    if modulus == 0 {
11935                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11936                    }
11937                    if remainder >= modulus {
11938                        return Err(self.err(format!(
11939                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11940                        )));
11941                    }
11942                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11943                }
11944                match self.peek() {
11945                    Token::From => {
11946                        self.advance();
11947                        let lower = Box::new(self.parse_partition_bound_expr()?);
11948                        if !matches!(self.peek(), Token::To) {
11949                            return Err(self.err(format!(
11950                                "expected TO after FROM (...), got {:?}",
11951                                self.peek()
11952                            )));
11953                        }
11954                        self.advance();
11955                        let upper = Box::new(self.parse_partition_bound_expr()?);
11956                        Ok(PartitionOfBoundsAst::Range { lower, upper })
11957                    }
11958                    Token::In => {
11959                        self.advance();
11960                        if !matches!(self.peek(), Token::LParen) {
11961                            return Err(self.err(format!(
11962                                "expected '(' after FOR VALUES IN, got {:?}",
11963                                self.peek()
11964                            )));
11965                        }
11966                        self.advance();
11967                        let mut values = Vec::new();
11968                        loop {
11969                            values.push(self.parse_expr(0)?);
11970                            match self.peek() {
11971                                Token::Comma => {
11972                                    self.advance();
11973                                }
11974                                Token::RParen => {
11975                                    self.advance();
11976                                    break;
11977                                }
11978                                other => {
11979                                    return Err(self.err(format!(
11980                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11981                                    )));
11982                                }
11983                            }
11984                        }
11985                        if values.is_empty() {
11986                            return Err(
11987                                self.err("FOR VALUES IN requires at least one literal".to_string())
11988                            );
11989                        }
11990                        Ok(PartitionOfBoundsAst::List { values })
11991                    }
11992                    other => Err(self.err(format!(
11993                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11994                    ))),
11995                }
11996            }
11997            other => Err(self.err(format!(
11998                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11999            ))),
12000        }
12001    }
12002
12003    /// v7.16.2 — peek for `information_schema.<tbl>` /
12004    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12005    /// three tokens + return a synthetic table name the engine's
12006    /// SELECT path recognises as a virtual view. Returns `None`
12007    /// when the head doesn't look like a meta-qualified name.
12008    /// Used by `parse_table_ref` to bypass the
12009    /// `expect_ident_like` schema-strip for these specific PG
12010    /// meta schemas (mailrs round-10 A.3).
12011    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12012        // Extract the schema name. Must be a plain ident token.
12013        let schema = match self.tokens.get(self.pos) {
12014            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12015            _ => return None,
12016        };
12017        // Dot.
12018        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12019            return None;
12020        }
12021        // The table-side ident may lex as a reserved keyword
12022        // (e.g. `Token::Tables`). Tolerate the common ones via a
12023        // helper that reads the trailing token's underlying name.
12024        let tbl = match self.tokens.get(self.pos + 2)? {
12025            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12026            Token::Tables => "tables".to_string(),
12027            // Other PG meta table names that may collide with
12028            // reserved keywords land here as needed.
12029            _ => return None,
12030        };
12031        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12032        // names so the synthetic name doesn't double-prefix
12033        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12034        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12035            ("__spg_info_", tbl.to_ascii_lowercase())
12036        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12037            // v7.39 (round 541) — only the catalogs SPG actually
12038            // synthesises are rewritten, which is what the BARE path
12039            // has always checked. Anything else keeps its own name and
12040            // takes the ordinary route: `pg_stat_activity` and friends
12041            // resolve through meta_view_result, and a name that is no
12042            // catalog at all gets PG's "relation does not exist"
12043            // instead of a message about a view SPG cannot materialise.
12044            let lowered = tbl.to_ascii_lowercase();
12045            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12046                self.advance(); // schema
12047                self.advance(); // dot
12048                self.advance(); // tbl
12049                return Some((lowered.clone(), lowered));
12050            }
12051            let bare = lowered
12052                .strip_prefix("pg_")
12053                .map(alloc::string::String::from)
12054                .unwrap_or(lowered);
12055            ("__spg_pg_", bare)
12056        } else if schema.eq_ignore_ascii_case("mysql") {
12057            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12058            // (`mysql.user`, `mysql.db`). Same synthetic-name
12059            // shape as pg_catalog.
12060            ("__spg_mysql_", tbl.to_ascii_lowercase())
12061        } else {
12062            return None;
12063        };
12064        self.advance(); // schema
12065        self.advance(); // dot
12066        self.advance(); // tbl
12067        Some((
12068            alloc::format!("{prefix}{normalised}"),
12069            tbl.to_ascii_lowercase(),
12070        ))
12071    }
12072
12073    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12074    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12075    /// implicit front of every search_path, so a bare reference to a
12076    /// known catalog table always means the catalog table. Only the
12077    /// names the engine actually synthesises are recognised — any
12078    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12079    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12080        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12081        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12082        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12083        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12084        // through the meta_view_result path instead, and already resolve
12085        // bare — they must NOT be listed here or the __spg_ rewrite would
12086        // mis-target them.)
12087        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12088        let name = match self.tokens.get(self.pos) {
12089            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12090            _ => return None,
12091        };
12092        // A following dot means this ident is a schema qualifier,
12093        // not a table name — let the qualified path handle it.
12094        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12095            return None;
12096        }
12097        if !PG_META_TABLES.contains(&name.as_str()) {
12098            return None;
12099        }
12100        self.advance();
12101        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12102        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12103    }
12104
12105    /// Consume a bare ident if its lowercase matches `kw`, else err.
12106    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12107    /// Peeks only; the caller advances.
12108    fn peek_keyword_ident(&self, kw: &str) -> bool {
12109        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12110    }
12111
12112    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12113        match self.advance() {
12114            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12115            other => Err(ParseError {
12116                message: format!("expected {kw:?}, got {other:?}"),
12117                token_pos: self.consumed_pos(),
12118            }),
12119        }
12120    }
12121
12122    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12123    /// literal (`'foo'`) — same shape used by CREATE USER for the
12124    /// username slot.
12125    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12126        match self.advance() {
12127            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12128            other => Err(ParseError {
12129                message: format!("expected identifier or string, got {other:?}"),
12130                token_pos: self.consumed_pos(),
12131            }),
12132        }
12133    }
12134
12135    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12136        match self.advance() {
12137            Token::String(s) => Ok(s),
12138            other => Err(ParseError {
12139                message: format!("expected quoted string, got {other:?}"),
12140                token_pos: self.consumed_pos(),
12141            }),
12142        }
12143    }
12144
12145    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12146        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12147        // subqueries recurse through here without passing
12148        // parse_expr; share the same nesting budget.
12149        self.enter_nested()?;
12150        let r = self.parse_select_stmt_inner();
12151        self.nest_depth -= 1;
12152        r
12153    }
12154
12155    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12156        // Caller dispatches on Token::Select; the inner helper handles
12157        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12158        // get a fresh bare-select parse and may not have their own ORDER
12159        // BY / LIMIT.
12160        let mut head = self.parse_bare_select()?;
12161        self.parse_setop_chain_into(&mut head)?;
12162        self.parse_select_tail_into(&mut head)?;
12163        Ok(Statement::Select(head))
12164    }
12165
12166    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12167    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12168    /// token), and INTERSECT [ALL] (a bare ident — it was never
12169    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12170    /// tighter than UNION / EXCEPT — the executor folds the chain
12171    /// left-to-right, which is already correct for LEADING
12172    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12173    /// pair nests into that previous peer, so A UNION B INTERSECT C
12174    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12175    /// groups.
12176    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12177        // A parenthesized group arrives with its own (already
12178        // regrouped) unions on `head`; only the pairs THIS chain
12179        // appends participate in the precedence regroup below —
12180        // nesting an outer INTERSECT into a group-internal peer
12181        // would dissolve the explicit grouping.
12182        let boundary = head.unions.len();
12183        loop {
12184            let base = match self.peek() {
12185                Token::Union => UnionKind::Distinct,
12186                Token::Except => UnionKind::Except,
12187                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12188                _ => break,
12189            };
12190            self.advance();
12191            let kind = if matches!(self.peek(), Token::All) {
12192                self.advance();
12193                match base {
12194                    UnionKind::Distinct => UnionKind::All,
12195                    UnionKind::Except => UnionKind::ExceptAll,
12196                    _ => UnionKind::IntersectAll,
12197                }
12198            } else {
12199                base
12200            };
12201            let peer = self.parse_bare_select()?;
12202            head.unions.push((kind, peer));
12203        }
12204        let mut pairs = core::mem::take(&mut head.unions);
12205        let tail = pairs.split_off(boundary);
12206        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12207        for (kind, peer) in tail {
12208            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12209            // An intersect nests into the previous element of THIS
12210            // chain only; with no new previous element it stays at
12211            // the outer level (the left fold applies it to the
12212            // whole head, group included).
12213            match (
12214                is_intersect,
12215                regrouped.len() > boundary,
12216                regrouped.last_mut(),
12217            ) {
12218                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12219                _ => regrouped.push((kind, peer)),
12220            }
12221        }
12222        head.unions = regrouped;
12223        Ok(())
12224    }
12225
12226    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12227    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12228    /// the top-level bare VALUES statement reuses it verbatim.
12229    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12230    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12231    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12232    /// where the grouping-set universe is still in scope.
12233    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12234        if !matches!(self.peek(), Token::Order) {
12235            return Ok(Vec::new());
12236        }
12237        self.advance();
12238        if !self.peek_is_by() {
12239            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12240        }
12241        self.advance();
12242        let mut keys = Vec::new();
12243        loop {
12244            // v7.39 (round 691) — save/restore, the discipline this parser
12245            // already uses around `pending_sample_preds`, so a subquery inside
12246            // a key neither inherits nor leaks the channel.
12247            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12248            let saved_coll = self.order_key_collation.take();
12249            let parsed = self.parse_expr(0);
12250            self.in_order_by_key = saved_flag;
12251            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12252            let expr = parsed?;
12253            let desc = if matches!(self.peek(), Token::Desc) {
12254                self.advance();
12255                true
12256            } else if matches!(self.peek(), Token::Asc) {
12257                self.advance();
12258                false
12259            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12260                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12261                // one ordering per type, so the btree comparison operators map
12262                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12263                // would need a custom operator class — honest error.
12264                self.advance();
12265                match self.advance() {
12266                    Token::Lt | Token::LtEq => false,
12267                    Token::Gt | Token::GtEq => true,
12268                    other => {
12269                        return Err(self.err(alloc::format!(
12270                            "ORDER BY USING supports the btree comparison \
12271                             operators (< <= > >=); got {other:?}"
12272                        )));
12273                    }
12274                }
12275            } else {
12276                false
12277            };
12278            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12279            let nulls_first = self.parse_optional_nulls_placement()?;
12280            keys.push(OrderBy {
12281                expr,
12282                desc,
12283                nulls_first,
12284                collation,
12285            });
12286            if matches!(self.peek(), Token::Comma) {
12287                self.advance();
12288            } else {
12289                break;
12290            }
12291        }
12292        Ok(keys)
12293    }
12294
12295    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12296        // v7.39 (round 135) — a grouping-set query may have already parsed +
12297        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12298        // no ORDER BY token is present, keep that pre-set order_by rather than
12299        // clobbering it with an empty list.
12300        let parsed_keys = self.parse_order_by_keys()?;
12301        head.order_by = if parsed_keys.is_empty() {
12302            core::mem::take(&mut head.order_by)
12303        } else {
12304            parsed_keys
12305        };
12306        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12307        // order. PG's grammar takes a limit clause and an offset clause
12308        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12309        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12310        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12311        // spelling died on `expected end of input, got Limit`.
12312        //
12313        // Each may appear at most once, and LIMIT and FETCH FIRST are
12314        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12315        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12316        // A second one is left unconsumed here, which the caller reports
12317        // as trailing input rather than silently taking the last.
12318        let mut saw_limit = false;
12319        let mut saw_offset = false;
12320        loop {
12321            if !saw_limit && matches!(self.peek(), Token::Limit) {
12322                self.advance();
12323                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12324                // PG synonyms for "no limit". Treat both as None
12325                // (no head.limit set) so the engine's existing
12326                // unlimited-result path takes over. Reject was the
12327                // pre-5.1 behaviour and broke pg_dump-flavoured
12328                // tooling that occasionally emits LIMIT NULL.
12329                if self.consume_limit_unbounded_sentinel() {
12330                    head.limit = None;
12331                } else {
12332                    let first = self.parse_limit_expr("LIMIT")?;
12333                    // MySQL `LIMIT offset, count` — the first number is
12334                    // the offset when a comma follows.
12335                    if matches!(self.peek(), Token::Comma) {
12336                        self.advance();
12337                        let count = self.parse_limit_expr("LIMIT")?;
12338                        head.offset = Some(first);
12339                        saw_offset = true;
12340                        head.limit = Some(count);
12341                    } else {
12342                        head.limit = Some(first);
12343                    }
12344                }
12345                saw_limit = true;
12346                continue;
12347            }
12348            if !saw_offset && matches!(self.peek(), Token::Offset) {
12349                self.advance();
12350                // PG also accepts an optional `ROW` / `ROWS` trailer
12351                // after the offset value (`OFFSET 10 ROWS`). The
12352                // FETCH-FIRST branch below relies on the same.
12353                let off = self.parse_limit_expr("OFFSET")?;
12354                self.consume_optional_rows_keyword();
12355                head.offset = Some(off);
12356                saw_offset = true;
12357                continue;
12358            }
12359            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12360            // the SQL-standard alias for LIMIT. PG accepts both
12361            // spellings interchangeably; pg_dump emits FETCH FIRST in
12362            // newer versions. We map it onto `head.limit` so the
12363            // engine path is unified.
12364            if !saw_limit
12365                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12366                    if s.eq_ignore_ascii_case("fetch"))
12367            {
12368                self.advance(); // FETCH
12369                // `FIRST` or `NEXT` (both legal per SQL standard).
12370                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12371                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12372                {
12373                    self.advance();
12374                }
12375                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12376                // implicit 1 — but we always consume one if present).
12377                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12378                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12379                {
12380                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12381                    crate::ast::LimitExpr::Literal(1)
12382                } else {
12383                    self.parse_limit_expr("FETCH FIRST")?
12384                };
12385                // Eat `ROW` / `ROWS` if not already consumed above.
12386                self.consume_optional_rows_keyword();
12387                // Optional `ONLY` (the spec form) — or the SQL:2008
12388                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12389                // now honours WITH TIES by extending past the LIMIT
12390                // truncation point through every row that shares the
12391                // last-kept row's ORDER BY key.
12392                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12393                    if s.eq_ignore_ascii_case("only"))
12394                {
12395                    self.advance();
12396                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12397                    if s.eq_ignore_ascii_case("with"))
12398                {
12399                    self.advance(); // WITH
12400                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12401                        if s.eq_ignore_ascii_case("ties"))
12402                    {
12403                        self.advance();
12404                        head.limit_with_ties = true;
12405                    }
12406                }
12407                head.limit = Some(count);
12408                saw_limit = true;
12409                continue;
12410            }
12411            break;
12412        }
12413        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12414        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12415        //       [ OF table_name [, …] ]
12416        //       [ NOWAIT | SKIP LOCKED ]
12417        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12418        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12419        // SELECT already returns a consistent snapshot — so these
12420        // are accept-and-discard: the parser absorbs them so
12421        // mailrs / Rails / Django code paths that emit `SELECT
12422        // … FOR UPDATE` for advisory pessimistic locking load
12423        // without a parser error. The on-disk locking model is
12424        // unchanged; callers that rely on FOR UPDATE for read-
12425        // through-write ordering still get the right answer
12426        // because SPG serialises writes anyway.
12427        head.locking = self
12428            .consume_optional_for_lock_clauses()
12429            .map(alloc::boxed::Box::new);
12430        Ok(())
12431    }
12432
12433    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12434    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12435    /// LOCKED ]` trailers. Each clause is fully accepted and
12436    /// discarded — SPG's single-writer model already satisfies the
12437    /// callers' implicit ordering requirement. Stops at the first
12438    /// token that isn't `FOR`.
12439    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12440        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12441        // not discarded. PG keeps the strongest of several clauses; the
12442        // policy of the last one wins, which is what this loop records.
12443        let mut seen: Option<crate::ast::LockingClause> = None;
12444        while matches!(self.peek(), Token::For) {
12445            // v7.37.14 (A2.5-stub) — record that this query asked
12446            // for a row lock the parser is about to silently
12447            // discard. Operators surface the count via
12448            // `spg_sql::silent_for_update_count()` so they can
12449            // gauge how much of the workload depends on advisory
12450            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12451            // before v7.37.15's per-row tuple locking lands.
12452            crate::record_silent_for_update_clause();
12453            self.advance(); // FOR
12454            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12455            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12456            let mut no_key = false;
12457            let mut key = false;
12458            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12459                if s.eq_ignore_ascii_case("no"))
12460            {
12461                self.advance(); // NO
12462                no_key = true;
12463                // The next ident should be KEY but be generous;
12464                // anything followed by UPDATE/SHARE is accepted.
12465                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12466                    if s.eq_ignore_ascii_case("key"))
12467                {
12468                    self.advance(); // KEY
12469                }
12470            }
12471            // `KEY` prefix (PG `FOR KEY SHARE`).
12472            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12473                if s.eq_ignore_ascii_case("key"))
12474            {
12475                self.advance(); // KEY
12476                key = true;
12477            }
12478            // Lock-strength keyword: UPDATE / SHARE. Required, but
12479            // we're lenient — an unexpected token here just bails
12480            // (we already consumed FOR; caller's downstream
12481            // dispatch will error if anything actually depends on
12482            // the trailing tokens).
12483            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12484                if s.eq_ignore_ascii_case("update"));
12485            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12486                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12487            {
12488                self.advance();
12489                use crate::ast::LockStrength as LS;
12490                let strength = match (is_update, no_key, key) {
12491                    (true, true, _) => LS::NoKeyUpdate,
12492                    (true, _, _) => LS::Update,
12493                    (false, _, true) => LS::KeyShare,
12494                    (false, _, _) => LS::Share,
12495                };
12496                seen = Some(crate::ast::LockingClause {
12497                    strength,
12498                    of_tables: alloc::vec::Vec::new(),
12499                    policy: crate::ast::LockWait::Wait,
12500                });
12501            } else {
12502                // FOR by itself (or `FOR KEY` with nothing after) —
12503                // give up on the lock-clause path. We've already
12504                // advanced past FOR; further attempts to parse
12505                // here would clobber state.
12506                return seen;
12507            }
12508            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12509            // joining and locking only a subset of tables.
12510            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12511                if s.eq_ignore_ascii_case("of"))
12512            {
12513                self.advance(); // OF
12514                #[allow(clippy::while_let_loop)]
12515                loop {
12516                    match self.peek() {
12517                        Token::Ident(_) | Token::QuotedIdent(_) => {
12518                            // v7.39 (round 294) — the name is CAPTURED now: PG
12519                            // validates it against the FROM clause, and an
12520                            // uncaptured list silently means "lock everything".
12521                            let mut nm = match self.advance() {
12522                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12523                                _ => alloc::string::String::new(),
12524                            };
12525                            // Optional schema-qualified `schema.table`.
12526                            if matches!(self.peek(), Token::Dot) {
12527                                self.advance();
12528                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12529                                {
12530                                    self.advance();
12531                                    nm = n;
12532                                }
12533                            }
12534                            if let Some(c) = seen.as_mut() {
12535                                c.of_tables.push(nm);
12536                            }
12537                        }
12538                        _ => break,
12539                    }
12540                    if matches!(self.peek(), Token::Comma) {
12541                        self.advance();
12542                    } else {
12543                        break;
12544                    }
12545                }
12546            }
12547            // Optional `NOWAIT` | `SKIP LOCKED`.
12548            match self.peek().clone() {
12549                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12550                    self.advance();
12551                    if let Some(c) = seen.as_mut() {
12552                        c.policy = crate::ast::LockWait::NoWait;
12553                    }
12554                }
12555                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12556                    self.advance(); // SKIP
12557                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12558                        if s.eq_ignore_ascii_case("locked"))
12559                    {
12560                        self.advance(); // LOCKED
12561                        if let Some(c) = seen.as_mut() {
12562                            c.policy = crate::ast::LockWait::SkipLocked;
12563                        }
12564                    }
12565                }
12566                _ => {}
12567            }
12568            // Loop: PG allows multiple FOR clauses chained.
12569        }
12570        seen
12571    }
12572
12573    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12574    /// Bind value gets resolved during prepared-statement Execute;
12575    /// the Pratt expression parser would over-accept here (e.g.
12576    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12577    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12578    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12579    /// when one was consumed; caller skips the regular
12580    /// limit-value parse and leaves `head.limit` at None.
12581    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12582        if matches!(self.peek(), Token::Null) {
12583            self.advance();
12584            return true;
12585        }
12586        if matches!(self.peek(), Token::All) {
12587            self.advance();
12588            return true;
12589        }
12590        false
12591    }
12592
12593    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12594    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12595    /// SQL-standard shape. No-op when missing.
12596    fn consume_optional_rows_keyword(&mut self) {
12597        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12598            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12599        {
12600            self.advance();
12601        }
12602    }
12603
12604    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12605    ///
12606    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12607    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12608    /// constant, which is why that spelling keeps the token path below.
12609    ///
12610    /// Constants are folded here rather than carried into the tree: the
12611    /// 15+ execution paths that read the row count go through
12612    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12613    /// means "no limit". A clause the engine could not resolve would
12614    /// therefore return the WHOLE table instead of failing. Folding at
12615    /// parse time keeps that impossible; a non-constant clause is still
12616    /// a clean error (recorded residual — closing it wants a resolution
12617    /// pre-pass on the simple-query path, where `substitute_placeholders`
12618    /// does not run).
12619    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12620        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12621        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12622        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12623        // ONLY` both work (its grammar takes a c_expr). Both measured
12624        // against PG 18.4 in round 305.
12625        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12626            return self.parse_limit_constant(label);
12627        }
12628        // One pass, no rewind: `advance()` takes each token by
12629        // `mem::replace`, so a consumed token reads back as Eof and this
12630        // parser cannot backtrack. Everything — bare literal included —
12631        // is therefore folded from the parsed expression rather than
12632        // re-read from the token stream.
12633        let start = self.pos;
12634        let e = self.parse_expr(0)?;
12635        if let crate::ast::Expr::Placeholder(n) = e {
12636            return Ok(crate::ast::LimitExpr::Placeholder(n));
12637        }
12638        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12639        match fold_limit_constant(&e) {
12640            Some(Ok(v)) if v < 0 => Err(ParseError {
12641                message: alloc::format!("{neg_label} must not be negative"),
12642                token_pos: start,
12643            }),
12644            Some(Ok(v)) => u32::try_from(v)
12645                .map(crate::ast::LimitExpr::Literal)
12646                .map_err(|_| ParseError {
12647                    message: alloc::format!("{label} value too large: {v}"),
12648                    token_pos: start,
12649                }),
12650            Some(Err(message)) => Err(ParseError {
12651                message: message.replace("{L}", neg_label),
12652                token_pos: start,
12653            }),
12654            // v7.39 (round 305, V23) — not foldable at parse time
12655            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12656            // expression; the engine evaluates it once before dispatch.
12657            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12658        }
12659    }
12660
12661    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12662        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12663        // coercion rules, not just an integer token: a NUMERIC rounds half
12664        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12665        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12666        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12667        // content, failing as an input-syntax error on the value. General
12668        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12669        // they need an Expr-carrying LimitExpr variant.
12670        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12671        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12672            message,
12673            token_pos: pos,
12674        };
12675        match self.advance() {
12676            Token::Integer(n) if n >= 0 => u32::try_from(n)
12677                .map(crate::ast::LimitExpr::Literal)
12678                .map_err(|_| ParseError {
12679                    message: alloc::format!("{label} value too large: {n}"),
12680                    token_pos: self.consumed_pos(),
12681                }),
12682            Token::Integer(_) => Err(err_at(
12683                alloc::format!("{neg_label} must not be negative"),
12684                self.pos.saturating_sub(1),
12685            )),
12686            Token::Numeric(t) => {
12687                let pos = self.pos.saturating_sub(1);
12688                let v: f64 = t.parse().map_err(|_| {
12689                    err_at(
12690                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12691                        pos,
12692                    )
12693                })?;
12694                if v < 0.0 {
12695                    return Err(err_at(
12696                        alloc::format!("{neg_label} must not be negative"),
12697                        pos,
12698                    ));
12699                }
12700                // Round half away from zero — PG's numeric→bigint cast.
12701                // (no_std: no f64::round; v is non-negative, so truncating
12702                // v + 0.5 is the same thing.)
12703                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12704                let rounded = (v + 0.5) as u64;
12705                u32::try_from(rounded)
12706                    .map(crate::ast::LimitExpr::Literal)
12707                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12708            }
12709            Token::Minus => {
12710                let pos = self.pos.saturating_sub(1);
12711                match self.peek() {
12712                    Token::Integer(_) | Token::Numeric(_) => {
12713                        self.advance();
12714                        Err(err_at(
12715                            alloc::format!("{neg_label} must not be negative"),
12716                            pos,
12717                        ))
12718                    }
12719                    other => Err(err_at(
12720                        alloc::format!(
12721                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12722                        ),
12723                        pos,
12724                    )),
12725                }
12726            }
12727            Token::String(t) => {
12728                let pos = self.pos.saturating_sub(1);
12729                match t.trim().parse::<i64>() {
12730                    Ok(n) if n < 0 => Err(err_at(
12731                        alloc::format!("{neg_label} must not be negative"),
12732                        pos,
12733                    )),
12734                    Ok(n) => u32::try_from(n)
12735                        .map(crate::ast::LimitExpr::Literal)
12736                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12737                    Err(_) => Err(err_at(
12738                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12739                        pos,
12740                    )),
12741                }
12742            }
12743            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12744            other => Err(ParseError {
12745                message: alloc::format!(
12746                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12747                ),
12748                token_pos: self.consumed_pos(),
12749            }),
12750        }
12751    }
12752
12753    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12754    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12755    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12756    /// `parse_select_stmt` is responsible for filling those in.
12757    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12758    /// call in the expression tree to the per-set integer bitmask
12759    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12760    /// is dropped in this grouping set). Runs during the ROLLUP /
12761    /// CUBE / GROUPING SETS expansion, where the set is known.
12762    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12763    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12764    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12765        if let Expr::FunctionCall { name, .. } = expr
12766            && name.eq_ignore_ascii_case("grouping")
12767        {
12768            if !out.iter().any(|e| e == expr) {
12769                out.push(expr.clone());
12770            }
12771            return;
12772        }
12773        match expr {
12774            Expr::Binary { lhs, rhs, .. } => {
12775                Self::collect_grouping_calls(lhs, out);
12776                Self::collect_grouping_calls(rhs, out);
12777            }
12778            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12779                Self::collect_grouping_calls(expr, out)
12780            }
12781            Expr::FunctionCall { args, .. } => {
12782                for a in args {
12783                    Self::collect_grouping_calls(a, out);
12784                }
12785            }
12786            Expr::Case {
12787                operand,
12788                branches,
12789                else_branch,
12790            } => {
12791                if let Some(o) = operand {
12792                    Self::collect_grouping_calls(o, out);
12793                }
12794                for (c, v) in branches {
12795                    Self::collect_grouping_calls(c, out);
12796                    Self::collect_grouping_calls(v, out);
12797                }
12798                if let Some(x) = else_branch {
12799                    Self::collect_grouping_calls(x, out);
12800                }
12801            }
12802            _ => {}
12803        }
12804    }
12805
12806    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12807    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12808    /// `__grp_ord_k` (injected per grouping-set branch).
12809    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12810        if let Expr::FunctionCall { name, .. } = expr
12811            && name.eq_ignore_ascii_case("grouping")
12812        {
12813            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12814                *expr = Expr::Column(crate::ast::ColumnName {
12815                    qualifier: None,
12816                    name: alloc::format!("__grp_ord_{k}"),
12817                });
12818            }
12819            return;
12820        }
12821        match expr {
12822            Expr::Binary { lhs, rhs, .. } => {
12823                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12824                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12825            }
12826            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12827                Self::rewrite_grouping_to_col(expr, grp_exprs)
12828            }
12829            Expr::FunctionCall { args, .. } => {
12830                for a in args {
12831                    Self::rewrite_grouping_to_col(a, grp_exprs);
12832                }
12833            }
12834            Expr::Case {
12835                operand,
12836                branches,
12837                else_branch,
12838            } => {
12839                if let Some(o) = operand {
12840                    Self::rewrite_grouping_to_col(o, grp_exprs);
12841                }
12842                for (c, v) in branches {
12843                    Self::rewrite_grouping_to_col(c, grp_exprs);
12844                    Self::rewrite_grouping_to_col(v, grp_exprs);
12845                }
12846                if let Some(x) = else_branch {
12847                    Self::rewrite_grouping_to_col(x, grp_exprs);
12848                }
12849            }
12850            _ => {}
12851        }
12852    }
12853
12854    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12855    /// as the list of key sets it contributes. A bare expression is one
12856    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12857    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12858    /// the concatenation of its items' sets, where an item is itself an
12859    /// element, a parenthesized key list, or the empty set `()`. A
12860    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12861    /// move together.
12862    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12863        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12864        // ROLLUP ( … ) / CUBE ( … )
12865        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12866            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12867        {
12868            let is_cube = is_kw(self.peek(), "cube");
12869            self.advance(); // ROLLUP / CUBE
12870            self.advance(); // (
12871            let mut units: Vec<Vec<Expr>> = Vec::new();
12872            loop {
12873                if matches!(self.peek(), Token::LParen) {
12874                    // Composite unit: (a, b) rolls up as one.
12875                    self.advance();
12876                    let mut unit = Vec::new();
12877                    if !matches!(self.peek(), Token::RParen) {
12878                        loop {
12879                            unit.push(self.parse_expr(0)?);
12880                            match self.peek() {
12881                                Token::Comma => {
12882                                    self.advance();
12883                                }
12884                                Token::RParen => break,
12885                                other => {
12886                                    return Err(self.err(format!(
12887                                        "expected ',' or ')' in grouping unit, got {other:?}"
12888                                    )));
12889                                }
12890                            }
12891                        }
12892                    }
12893                    self.advance(); // )
12894                    units.push(unit);
12895                } else {
12896                    units.push(alloc::vec![self.parse_expr(0)?]);
12897                }
12898                match self.peek() {
12899                    Token::Comma => {
12900                        self.advance();
12901                    }
12902                    Token::RParen => break,
12903                    other => {
12904                        return Err(self.err(format!(
12905                            "expected ',' or ')' in grouping list, got {other:?}"
12906                        )));
12907                    }
12908                }
12909            }
12910            self.advance(); // )
12911            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12912                units
12913                    .iter()
12914                    .zip(unit_sel.iter())
12915                    .filter(|(_, keep)| **keep)
12916                    .flat_map(|(u, _)| u.iter().cloned())
12917                    .collect()
12918            };
12919            let n = units.len();
12920            if is_cube {
12921                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12922                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12923                    .collect();
12924                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12925                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12926            }
12927            return Ok((0..=n)
12928                .rev()
12929                .map(|keep| {
12930                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12931                    flatten(&sel)
12932                })
12933                .collect());
12934        }
12935        // GROUPING SETS ( item [, item]* )
12936        if is_kw(self.peek(), "grouping")
12937            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12938        {
12939            self.advance(); // GROUPING
12940            self.advance(); // SETS
12941            if !matches!(self.peek(), Token::LParen) {
12942                return Err(self.err(format!(
12943                    "expected '(' after GROUPING SETS, got {:?}",
12944                    self.peek()
12945                )));
12946            }
12947            self.advance(); // outer (
12948            let mut sets: Vec<Vec<Expr>> = Vec::new();
12949            loop {
12950                if matches!(self.peek(), Token::LParen) {
12951                    // A parenthesized key list (or the empty set).
12952                    self.advance();
12953                    let mut set = Vec::new();
12954                    if !matches!(self.peek(), Token::RParen) {
12955                        loop {
12956                            set.push(self.parse_expr(0)?);
12957                            match self.peek() {
12958                                Token::Comma => {
12959                                    self.advance();
12960                                }
12961                                Token::RParen => break,
12962                                other => {
12963                                    return Err(self.err(format!(
12964                                        "expected ',' or ')' in grouping set, got {other:?}"
12965                                    )));
12966                                }
12967                            }
12968                        }
12969                    }
12970                    self.advance(); // )
12971                    sets.push(set);
12972                } else {
12973                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12974                    // bare expression.
12975                    sets.extend(self.parse_grouping_element()?);
12976                }
12977                match self.peek() {
12978                    Token::Comma => {
12979                        self.advance();
12980                    }
12981                    Token::RParen => break,
12982                    other => {
12983                        return Err(self.err(format!(
12984                            "expected ',' or ')' after a grouping set, got {other:?}"
12985                        )));
12986                    }
12987                }
12988            }
12989            self.advance(); // outer )
12990            return Ok(sets);
12991        }
12992        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12993    }
12994
12995    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12996        // v7.38 (read01) — a reference to a key that is dropped in this grouping
12997        // set evaluates to NULL, at any depth. Previously only a *top-level*
12998        // select item equal to a dropped key was nullified, so a key nested in
12999        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13000        // column and failed to resolve against the set's synthetic schema.
13001        if dropped.iter().any(|d| d == expr) {
13002            *expr = Expr::Literal(Literal::Null);
13003            return;
13004        }
13005        if let Expr::FunctionCall { name, args } = expr
13006            && name.eq_ignore_ascii_case("grouping")
13007        {
13008            let mut mask: i64 = 0;
13009            for a in args.iter() {
13010                mask <<= 1;
13011                if dropped.iter().any(|d| d == a) {
13012                    mask |= 1;
13013                }
13014            }
13015            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13016            // literal: a bare integer in a select item is indistinguishable
13017            // from a positional reference once `ORDER BY 1` substitutes the
13018            // item back in, and the round-232 position check then read the
13019            // mask value as an out-of-range position. The cast changes
13020            // nothing semantically (grouping() is integer).
13021            *expr = Expr::Cast {
13022                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13023                target: crate::ast::CastTarget::Int,
13024            };
13025            return;
13026        }
13027        // Generic recursion over the common expression shapes the
13028        // SELECT list uses; anything without child expressions is
13029        // left alone.
13030        match expr {
13031            Expr::FunctionCall { args, .. } => {
13032                for a in args {
13033                    Self::substitute_grouping_calls(a, dropped);
13034                }
13035            }
13036            Expr::Binary { lhs, rhs, .. } => {
13037                Self::substitute_grouping_calls(lhs, dropped);
13038                Self::substitute_grouping_calls(rhs, dropped);
13039            }
13040            Expr::Unary { expr: inner, .. } => {
13041                Self::substitute_grouping_calls(inner, dropped);
13042            }
13043            Expr::Cast { expr: inner, .. } => {
13044                Self::substitute_grouping_calls(inner, dropped);
13045            }
13046            Expr::Case {
13047                operand,
13048                branches,
13049                else_branch,
13050            } => {
13051                if let Some(op) = operand {
13052                    Self::substitute_grouping_calls(op, dropped);
13053                }
13054                for (w, t) in branches {
13055                    Self::substitute_grouping_calls(w, dropped);
13056                    Self::substitute_grouping_calls(t, dropped);
13057                }
13058                if let Some(e) = else_branch {
13059                    Self::substitute_grouping_calls(e, dropped);
13060                }
13061            }
13062            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13063            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13064            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13065            // …` is the canonical rollup-total label idiom).
13066            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13067            Expr::Like { expr, pattern, .. } => {
13068                Self::substitute_grouping_calls(expr, dropped);
13069                Self::substitute_grouping_calls(pattern, dropped);
13070            }
13071            Expr::InList { expr, list, .. } => {
13072                Self::substitute_grouping_calls(expr, dropped);
13073                for item in list {
13074                    Self::substitute_grouping_calls(item, dropped);
13075                }
13076            }
13077            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13078            Expr::Array(items) => {
13079                for item in items {
13080                    Self::substitute_grouping_calls(item, dropped);
13081                }
13082            }
13083            Expr::ArraySubscript { target, index } => {
13084                Self::substitute_grouping_calls(target, dropped);
13085                Self::substitute_grouping_calls(index, dropped);
13086            }
13087            Expr::ArraySlice { target, lo, hi } => {
13088                Self::substitute_grouping_calls(target, dropped);
13089                if let Some(lo) = lo {
13090                    Self::substitute_grouping_calls(lo, dropped);
13091                }
13092                if let Some(hi) = hi {
13093                    Self::substitute_grouping_calls(hi, dropped);
13094                }
13095            }
13096            Expr::AnyAll { expr, array, .. } => {
13097                Self::substitute_grouping_calls(expr, dropped);
13098                Self::substitute_grouping_calls(array, dropped);
13099            }
13100            _ => {}
13101        }
13102    }
13103
13104    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13105        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13106        // group: `( <select chain> )` usable anywhere a query block
13107        // is (head or peer of an outer chain). The group's own
13108        // unions ride the returned SelectStatement; the executor's
13109        // nested-peer recursion runs them.
13110        if matches!(self.peek(), Token::LParen)
13111            && matches!(
13112                self.tokens.get(self.pos + 1),
13113                Some(Token::Select | Token::LParen | Token::Values)
13114            )
13115        {
13116            self.advance(); // (
13117            self.enter_nested()?;
13118            // v7.37 D.20 — a group whose head is a VALUES list:
13119            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13120            // otherwise recurse into a nested SELECT/group head.
13121            let mut head = (if matches!(self.peek(), Token::Values) {
13122                self.advance(); // VALUES
13123                self.parse_values_rows_body()
13124            } else {
13125                self.parse_bare_select()
13126            })
13127            .and_then(|mut h| {
13128                self.parse_setop_chain_into(&mut h)?;
13129                Ok(h)
13130            });
13131            self.nest_depth -= 1;
13132            let mut head = match &mut head {
13133                Ok(h) => core::mem::take(h),
13134                Err(_) => return head,
13135            };
13136            // v7.37.17 (17.6 siblings) — group-internal tail:
13137            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13138            // group head, then wrap the group as a derived table
13139            // (SELECT * FROM (group)) so the outer chain / outer
13140            // tail can't clobber the group's own ordering or limit.
13141            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13142                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13143                    if s.eq_ignore_ascii_case("fetch"));
13144            if has_tail {
13145                self.parse_select_tail_into(&mut head)?;
13146                head = SelectStatement {
13147                    locking: None,
13148                    ctes: Vec::new(),
13149                    distinct: false,
13150                    distinct_on: Vec::new(),
13151                    items: alloc::vec![SelectItem::Wildcard],
13152                    from: Some(FromClause {
13153                        primary: TableRef {
13154                            name: "subquery".to_string(),
13155                            alias: None,
13156                            only: false,
13157                            as_of_segment: None,
13158                            unnest_expr: None,
13159                            unnest_column_aliases: Vec::new(),
13160                            with_ordinality: false,
13161                            generate_series_args: None,
13162                            lateral_subquery: Some(Box::new(head)),
13163                            jsonb_each_text_arg: None,
13164                            table_fn_call: None,
13165                            rows_from: None,
13166                            json_table: None,
13167                            scalar_fn_item: false,
13168                        },
13169                        joins: Vec::new(),
13170                    }),
13171                    where_: None,
13172                    group_by: None,
13173                    group_by_all: false,
13174                    having: None,
13175                    unions: Vec::new(),
13176                    order_by: Vec::new(),
13177                    limit: None,
13178                    offset: None,
13179                    limit_with_ties: false,
13180                    window_check_exprs: Vec::new(),
13181                };
13182            }
13183            if !matches!(self.peek(), Token::RParen) {
13184                return Err(self.err(format!(
13185                    "expected ')' after parenthesized query group, got {:?}",
13186                    self.peek()
13187                )));
13188            }
13189            self.advance();
13190            return Ok(head);
13191        }
13192        // `TABLE name` shorthand as a query block — valid anywhere
13193        // a SELECT head is (set-op peers included).
13194        if matches!(self.peek(), Token::Table)
13195            && matches!(
13196                self.tokens.get(self.pos + 1),
13197                Some(Token::Ident(_) | Token::QuotedIdent(_))
13198            )
13199        {
13200            return self.parse_table_shorthand();
13201        }
13202        if !matches!(self.peek(), Token::Select) {
13203            return Err(self.err(format!(
13204                "expected SELECT to start a query block, got {:?}",
13205                self.peek()
13206            )));
13207        }
13208        self.advance();
13209        let distinct = if matches!(self.peek(), Token::Distinct) {
13210            self.advance();
13211            true
13212        } else {
13213            false
13214        };
13215        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13216        // keep the first row (per ORDER BY) of each group the
13217        // expressions define. Django's .distinct('field') shape.
13218        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13219            self.advance(); // ON
13220            if !matches!(self.peek(), Token::LParen) {
13221                return Err(self.err(format!(
13222                    "expected '(' after DISTINCT ON, got {:?}",
13223                    self.peek()
13224                )));
13225            }
13226            self.advance();
13227            let mut exprs = Vec::new();
13228            loop {
13229                exprs.push(self.parse_expr(0)?);
13230                match self.peek() {
13231                    Token::Comma => {
13232                        self.advance();
13233                    }
13234                    Token::RParen => break,
13235                    other => {
13236                        return Err(self.err(format!(
13237                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13238                        )));
13239                    }
13240                }
13241            }
13242            self.advance(); // )
13243            exprs
13244        } else {
13245            Vec::new()
13246        };
13247        let mut items = self.parse_select_list()?;
13248        // Scope the TABLESAMPLE lowering channel to this SELECT:
13249        // stash whatever an enclosing select accumulated, collect
13250        // our own FROM's predicates, restore after the combine.
13251        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13252        let mut from = if matches!(self.peek(), Token::From) {
13253            self.advance();
13254            Some(self.parse_from_clause()?)
13255        } else {
13256            None
13257        };
13258        // v7.37 D.22 — a set-returning function in the projection with no FROM
13259        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13260        // rows. Move the first SRF projection item to a FROM-position derived
13261        // table and replace it in the projection with a reference to its output
13262        // column; sibling scalar columns repeat per SRF row. PG names the output
13263        // column after the function (or its AS alias). Reuses the FROM-SRF
13264        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13265        // works via the targetlist-SRF path.
13266        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13267        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13268        // is exactly what the function's own row shape already is. Anywhere else
13269        // (per outer row, or beside other items) it would need a real record-typed
13270        // projection, so it says so rather than answering something else.
13271        if let [
13272            SelectItem::Expr {
13273                expr: Expr::FunctionCall { name, args },
13274                ..
13275            },
13276        ] = items.as_slice()
13277            && name == "__record_expand"
13278        {
13279            let Some(Expr::FunctionCall {
13280                name: inner_name,
13281                args: inner_args,
13282            }) = args.first()
13283            else {
13284                return Err(self.err(
13285                    "(<expr>).* expands a function's record — it needs a function call".into(),
13286                ));
13287            };
13288            if from.is_some() {
13289                return Err(self.err(
13290                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13291                        .into(),
13292                ));
13293            }
13294            let fn_ref = TableRef {
13295                name: inner_name.clone(),
13296                alias: None,
13297                only: false,
13298                as_of_segment: None,
13299                unnest_expr: None,
13300                unnest_column_aliases: Vec::new(),
13301                with_ordinality: false,
13302                generate_series_args: None,
13303                lateral_subquery: None,
13304                jsonb_each_text_arg: None,
13305                table_fn_call: Some(Box::new((
13306                    inner_name.to_ascii_lowercase(),
13307                    inner_args.clone(),
13308                ))),
13309                rows_from: None,
13310                json_table: None,
13311                scalar_fn_item: false,
13312            };
13313            items = alloc::vec![SelectItem::Wildcard];
13314            from = Some(FromClause {
13315                primary: fn_ref,
13316                joins: Vec::new(),
13317            });
13318        }
13319        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13320        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13321        // record's fields takes the catalog. It becomes a LATERAL of the same
13322        // function plus one item per declared column — the machinery rounds 65
13323        // and 69 already built.
13324        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13325        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13326        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13327        // express, since the lifted one becomes a scan and the other would
13328        // expand per its rows (a cross product, not a zip). So when the
13329        // projection holds more than one top-level function call, the lift steps
13330        // aside and the engine's target-list expansion takes the whole list.
13331        let fn_call_items = items
13332            .iter()
13333            .filter(|it| {
13334                matches!(
13335                    it,
13336                    SelectItem::Expr {
13337                        expr: Expr::FunctionCall { .. },
13338                        ..
13339                    }
13340                )
13341            })
13342            .count();
13343        if from.is_none() && fn_call_items <= 1 {
13344            let mut found: Option<(usize, TableRef, String)> = None;
13345            for (i, item) in items.iter().enumerate() {
13346                if let SelectItem::Expr {
13347                    expr: Expr::FunctionCall { name, args },
13348                    alias,
13349                } = item
13350                {
13351                    let lname = name.to_ascii_lowercase();
13352                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13353                    let (unnest, gs) = match lname.as_str() {
13354                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13355                        "generate_series" if (2..=3).contains(&args.len()) => {
13356                            (None, Some(args.clone()))
13357                        }
13358                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13359                        // no-FROM projection yields the 1-based subscripts, i.e.
13360                        // generate_series(1, array_length(arr, dim)); an invalid
13361                        // dimension makes array_length NULL → 0 rows, as in PG.
13362                        "generate_subscripts" if args.len() == 2 => (
13363                            None,
13364                            Some(alloc::vec![
13365                                Expr::Literal(Literal::Integer(1)),
13366                                Expr::FunctionCall {
13367                                    name: "array_length".to_string(),
13368                                    args: args.clone(),
13369                                },
13370                            ]),
13371                        ),
13372                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13373                        // in a no-FROM projection unnest their *_to_array form.
13374                        "string_to_table" | "regexp_split_to_table" => {
13375                            let array_fn = if lname == "string_to_table" {
13376                                "string_to_array"
13377                            } else {
13378                                "regexp_split_to_array"
13379                            };
13380                            (
13381                                Some(Box::new(Expr::FunctionCall {
13382                                    name: array_fn.to_string(),
13383                                    args: args.clone(),
13384                                })),
13385                                None,
13386                            )
13387                        }
13388                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13389                        // a no-FROM projection expand per element. The scalar form
13390                        // returns the elements as a TEXT array, so unnest over the
13391                        // same call materialises one row each (same rewrite the
13392                        // FROM-clause form uses).
13393                        "jsonb_array_elements"
13394                        | "json_array_elements"
13395                        | "jsonb_array_elements_text"
13396                        | "json_array_elements_text"
13397                            if args.len() == 1 =>
13398                        {
13399                            (
13400                                Some(Box::new(Expr::FunctionCall {
13401                                    name: lname.clone(),
13402                                    args: args.clone(),
13403                                })),
13404                                None,
13405                            )
13406                        }
13407                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13408                        // in a no-FROM projection expands per match (scalar form
13409                        // returns the matches as a TEXT array → unnest).
13410                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13411                            Some(Box::new(Expr::FunctionCall {
13412                                name: lname.clone(),
13413                                args: args.clone(),
13414                            })),
13415                            None,
13416                        ),
13417                        _ => continue,
13418                    };
13419                    found = Some((
13420                        i,
13421                        TableRef {
13422                            name: colname.clone(),
13423                            alias: Some(colname.clone()),
13424                            only: false,
13425                            as_of_segment: None,
13426                            unnest_expr: unnest,
13427                            unnest_column_aliases: alloc::vec![colname.clone()],
13428                            with_ordinality: false,
13429                            generate_series_args: gs,
13430                            lateral_subquery: None,
13431                            jsonb_each_text_arg: None,
13432                            table_fn_call: None,
13433                            rows_from: None,
13434                            json_table: None,
13435                            scalar_fn_item: false,
13436                        },
13437                        colname,
13438                    ));
13439                    break;
13440                }
13441            }
13442            if let Some((idx, tref, colname)) = found {
13443                from = Some(FromClause {
13444                    primary: tref,
13445                    joins: Vec::new(),
13446                });
13447                items[idx] = SelectItem::Expr {
13448                    expr: Expr::Column(ColumnName {
13449                        qualifier: None,
13450                        name: colname.clone(),
13451                    }),
13452                    alias: Some(colname),
13453                };
13454            }
13455        }
13456        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13457        let where_ = if matches!(self.peek(), Token::Where) {
13458            self.advance();
13459            Some(self.parse_expr(0)?)
13460        } else {
13461            None
13462        };
13463        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13464            Some(match acc {
13465                Some(w) => Expr::Binary {
13466                    lhs: Box::new(pred),
13467                    op: crate::ast::BinOp::And,
13468                    rhs: Box::new(w),
13469                },
13470                None => pred,
13471            })
13472        });
13473        self.pending_sample_preds = enclosing_sample_preds;
13474        let mut group_by_all = false;
13475        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13476        // share one expansion: `grouping_sets` lists the key subsets
13477        // (first = primary, assigned to stmt.group_by; the rest
13478        // become UNION ALL peers), `grouping_universe` is the full
13479        // key list used to compute each peer's dropped keys.
13480        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13481        let mut grouping_universe: Vec<Expr> = Vec::new();
13482        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13483        // A BOOL, not the key list: this frame is the statement parser's, and
13484        // round 430 measured that a `Vec` local here is enough on its own to
13485        // tip the 512 KiB nesting guard. The keys are recoverable from
13486        // `grouping_universe`, which a rollup fills with exactly them.
13487        let mut mysql_rollup = false;
13488        let group_by = if matches!(self.peek(), Token::Group) {
13489            self.advance();
13490            if !self.peek_is_by() {
13491                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13492            }
13493            self.advance();
13494            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13495            // every non-aggregate SELECT-list item later.
13496            if matches!(self.peek(), Token::All) {
13497                self.advance();
13498                group_by_all = true;
13499                None
13500            } else {
13501                // v7.39 (round 242) — PG's general grouping-element grammar:
13502                // GROUP BY [DISTINCT] element [, element]*, where an element
13503                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13504                // SETS (…) — mixed freely. Each element yields a list of
13505                // key sets; the query's grouping sets are the CARTESIAN
13506                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13507                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13508                // content. ROLLUP/CUBE members may be composite
13509                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13510                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13511                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13512                // clause.
13513                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13514                    self.advance();
13515                    true
13516                } else {
13517                    false
13518                };
13519                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13520                loop {
13521                    element_sets.push(self.parse_grouping_element()?);
13522                    if matches!(self.peek(), Token::Comma) {
13523                        self.advance();
13524                    } else {
13525                        break;
13526                    }
13527                }
13528                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13529                for el in &element_sets {
13530                    let mut next: Vec<Vec<Expr>> = Vec::new();
13531                    for base in &total {
13532                        for set in el {
13533                            let mut merged = base.clone();
13534                            for k in set {
13535                                if !merged.iter().any(|m| m == k) {
13536                                    merged.push(k.clone());
13537                                }
13538                            }
13539                            next.push(merged);
13540                        }
13541                    }
13542                    total = next;
13543                }
13544                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13545                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13546                // The keys and the aggregates come out identical; the ROW
13547                // ORDER does not, and that is the part a report depends on.
13548                // MySQL interleaves each group's subtotal right after its
13549                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13550                // where the union-of-grouping-sets expansion emits every
13551                // leaf first and then every subtotal. MariaDB REFUSES an
13552                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13553                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13554                // agree on the order and disagree only on whether ORDER BY
13555                // is allowed (MySQL allows it; SPG allows it too, since
13556                // refusing would break the clients that can write it).
13557                if self.mysql_dialect
13558                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13559                    && matches!(
13560                        self.tokens.get(self.pos + 1),
13561                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13562                    )
13563                {
13564                    self.advance(); // WITH
13565                    self.advance(); // ROLLUP
13566                    let keys = total.into_iter().next().unwrap_or_default();
13567                    mysql_rollup = true;
13568                    // n+1 prefixes, largest first — the same expansion
13569                    // `ROLLUP (…)` produces.
13570                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13571                }
13572                if distinct_sets {
13573                    let mut seen: Vec<Vec<String>> = Vec::new();
13574                    total.retain(|set| {
13575                        let mut key: Vec<String> =
13576                            set.iter().map(|e| alloc::format!("{e}")).collect();
13577                        key.sort();
13578                        if seen.contains(&key) {
13579                            false
13580                        } else {
13581                            seen.push(key);
13582                            true
13583                        }
13584                    });
13585                }
13586                if total.len() > 1 {
13587                    let mut universe: Vec<Expr> = Vec::new();
13588                    for set in &total {
13589                        for k in set {
13590                            if !universe.iter().any(|u| u == k) {
13591                                universe.push(k.clone());
13592                            }
13593                        }
13594                    }
13595                    grouping_universe = universe;
13596                    let primary = total[0].clone();
13597                    grouping_sets = total;
13598                    Some(primary)
13599                } else {
13600                    // One set (a plain GROUP BY list, or a single-set
13601                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13602                    // single set — GROUPING SETS (()) — stays
13603                    // `Some(vec![])`: the grand-total group, which must
13604                    // run the aggregate path.
13605                    Some(total.into_iter().next().unwrap_or_default())
13606                }
13607            }
13608        } else {
13609            None
13610        };
13611        let having = if matches!(self.peek(), Token::Having) {
13612            self.advance();
13613            Some(self.parse_expr(0)?)
13614        } else {
13615            None
13616        };
13617        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13618        // OVER w parsed to a marker above; inline each definition
13619        // into the referencing WindowFunction nodes.
13620        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13621        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13622            self.advance();
13623            loop {
13624                let wname = self.expect_ident_like()?;
13625                if !matches!(self.peek(), Token::As) {
13626                    return Err(self.err(format!(
13627                        "expected AS after WINDOW {wname}, got {:?}",
13628                        self.peek()
13629                    )));
13630                }
13631                self.advance();
13632                // v7.39 (round 229) — PG rejects a redefinition outright.
13633                if window_defs
13634                    .iter()
13635                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13636                {
13637                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13638                }
13639                let def = self.parse_over_clause()?;
13640                // A definition may itself copy an earlier one
13641                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13642                // so resolve it against the defs already in scope. Same
13643                // copy rules as an `OVER (w1 …)` in the select list.
13644                let mut probe = Expr::WindowFunction {
13645                    name: String::new(),
13646                    args: Vec::new(),
13647                    partition_by: def.0,
13648                    order_by: def.1,
13649                    frame: def.2,
13650                    null_treatment: crate::ast::NullTreatment::Respect,
13651                    filter: None,
13652                };
13653                Self::substitute_named_windows(&mut probe, &window_defs)
13654                    .map_err(|m| self.err(m))?;
13655                let Expr::WindowFunction {
13656                    partition_by,
13657                    order_by,
13658                    frame,
13659                    ..
13660                } = probe
13661                else {
13662                    unreachable!("probe is a WindowFunction")
13663                };
13664                window_defs.push((wname, (partition_by, order_by, frame)));
13665                if matches!(self.peek(), Token::Comma) {
13666                    self.advance();
13667                    continue;
13668                }
13669                break;
13670            }
13671        }
13672        // v7.39 (round 705) — which definitions did anything reference?
13673        // The ones nothing did used to be dropped here, unexamined, so
13674        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13675        // definition whether referenced or not. Their key expressions ride
13676        // out on the statement for the engine to resolve.
13677        let mut window_refs: Vec<String> = Vec::new();
13678        if !window_defs.is_empty() {
13679            for it in &items {
13680                if let SelectItem::Expr { expr, .. } = it {
13681                    Self::collect_named_window_refs(expr, &mut window_refs);
13682                }
13683            }
13684        }
13685        let window_check_exprs: Vec<Expr> = window_defs
13686            .iter()
13687            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13688            .flat_map(|(_, (partition, order, _))| {
13689                partition
13690                    .iter()
13691                    .cloned()
13692                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13693            })
13694            .collect();
13695        if !window_defs.is_empty()
13696            || items
13697                .iter()
13698                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13699        {
13700            for it in &mut items {
13701                if let SelectItem::Expr { expr, .. } = it {
13702                    Self::substitute_named_windows(expr, &window_defs)
13703                        .map_err(|m| self.err(m))?;
13704                }
13705            }
13706        }
13707        // `GROUP BY 1` — positional keys substitute with the Nth
13708        // select item's expression (same contract ORDER BY has had
13709        // since v6.x). Out-of-range positions error.
13710        let group_by = match group_by {
13711            Some(mut keys) => {
13712                for k in &mut keys {
13713                    if let Expr::Literal(Literal::Integer(n)) = k {
13714                        let idx = *n;
13715                        if idx < 1 || idx as usize > items.len() {
13716                            return Err(self.err(alloc::format!(
13717                                "GROUP BY position {idx} is not in select list"
13718                            )));
13719                        }
13720                        match &items[(idx - 1) as usize] {
13721                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13722                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13723                                return Err(self.err(alloc::format!(
13724                                    "GROUP BY position {idx} references a wildcard item"
13725                                )));
13726                            }
13727                        }
13728                    }
13729                }
13730                Some(keys)
13731            }
13732            None => None,
13733        };
13734        let mut stmt = SelectStatement {
13735            locking: None,
13736            ctes: Vec::new(),
13737            distinct,
13738            distinct_on,
13739            items,
13740            from,
13741            where_,
13742            group_by,
13743            group_by_all,
13744            having,
13745            unions: Vec::new(),
13746            order_by: Vec::new(),
13747            limit: None,
13748            offset: None,
13749            limit_with_ties: false,
13750            window_check_exprs,
13751        };
13752        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13753        // first set is the primary (already on stmt.group_by); each
13754        // further set becomes a UNION ALL peer with its dropped
13755        // keys (universe minus the set) replaced by NULL literals
13756        // in the peer's items and group_by. PG-legal: non-grouped
13757        // select items must be group keys or aggregates, so a
13758        // dropped key's occurrences in the projection are exactly
13759        // the ones to nullify.
13760        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13761        // over a plain GROUP BY (every argument must be a group key; the
13762        // mask is then 0) and rejects anything else with 42803. SPG's
13763        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13764        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13765        // function `grouping`".
13766        if grouping_sets.len() <= 1 {
13767            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13768            let mut calls: Vec<Expr> = Vec::new();
13769            for item in &stmt.items {
13770                if let SelectItem::Expr { expr, .. } = item {
13771                    Self::collect_grouping_calls(expr, &mut calls);
13772                }
13773            }
13774            if let Some(h) = &stmt.having {
13775                Self::collect_grouping_calls(h, &mut calls);
13776            }
13777            for call in &calls {
13778                let Expr::FunctionCall { args, .. } = call else {
13779                    continue;
13780                };
13781                for a in args {
13782                    if !keys.iter().any(|k| k == a) {
13783                        return Err(self.err(
13784                            "arguments to GROUPING must be grouping expressions of the associated query level"
13785                                .to_string(),
13786                        ));
13787                    }
13788                }
13789            }
13790            if !calls.is_empty() {
13791                for item in &mut stmt.items {
13792                    if let SelectItem::Expr { expr, .. } = item {
13793                        Self::substitute_grouping_calls(expr, &[]);
13794                    }
13795                }
13796                if let Some(h) = &mut stmt.having {
13797                    Self::substitute_grouping_calls(h, &[]);
13798                }
13799            }
13800        }
13801        if grouping_sets.len() > 1 {
13802            // The primary set's own dropped keys nullify in the
13803            // HEAD's projection too (GROUPING SETS's first set may
13804            // omit keys other sets use).
13805            let primary = grouping_sets[0].clone();
13806            let head_dropped: Vec<Expr> = grouping_universe
13807                .iter()
13808                .filter(|u| !primary.iter().any(|k| k == *u))
13809                .cloned()
13810                .collect();
13811            for set in grouping_sets.iter().skip(1) {
13812                let mut peer = stmt.clone();
13813                peer.unions = Vec::new();
13814                let dropped: Vec<&Expr> = grouping_universe
13815                    .iter()
13816                    .filter(|u| !set.iter().any(|k| k == *u))
13817                    .collect();
13818                // Empty set = grand-total group: `Some(vec![])` forces
13819                // the aggregate path (one collapsed row) instead of a
13820                // per-row passthrough. See the primary-set note above.
13821                peer.group_by = Some(set.clone());
13822                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13823                for item in &mut peer.items {
13824                    if let SelectItem::Expr { expr, alias } = item {
13825                        if dropped.iter().any(|d| *d == expr) {
13826                            // v7.39 — keep the dropped key's name on the
13827                            // NULL literal so the UNION output column
13828                            // (and any top-level ORDER BY on it) still
13829                            // resolves.
13830                            if alias.is_none()
13831                                && let Expr::Column(c) = &expr
13832                            {
13833                                *alias = Some(c.name.clone());
13834                            }
13835                            *expr = Expr::Literal(Literal::Null);
13836                        } else {
13837                            Self::substitute_grouping_calls(expr, &dropped_owned);
13838                        }
13839                    }
13840                }
13841                if let Some(h) = &mut peer.having {
13842                    Self::substitute_grouping_calls(h, &dropped_owned);
13843                }
13844                stmt.unions.push((UnionKind::All, peer));
13845            }
13846            for item in &mut stmt.items {
13847                if let SelectItem::Expr { expr, alias } = item {
13848                    if head_dropped.iter().any(|d| d == expr) {
13849                        if alias.is_none()
13850                            && let Expr::Column(c) = &expr
13851                        {
13852                            *alias = Some(c.name.clone());
13853                        }
13854                        *expr = Expr::Literal(Literal::Null);
13855                    } else {
13856                        Self::substitute_grouping_calls(expr, &head_dropped);
13857                    }
13858                }
13859            }
13860            if let Some(h) = &mut stmt.having {
13861                Self::substitute_grouping_calls(h, &head_dropped);
13862            }
13863            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13864            // (while `grouping_universe` / the per-branch sets are in scope). For
13865            // each grouping() call in it, inject a per-branch hidden column
13866            // `__grp_ord_K` carrying that branch's mask into the head + every
13867            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13868            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13869            // from the final output. A standalone grouping-set query has ORDER BY
13870            // (not an explicit set-op) next, so consuming it here is safe.
13871            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13872            // rollup carries the hierarchical order: sort by the grouping
13873            // keys with the rolled-up NULLs last, which is exactly the
13874            // interleaving both oracles emit. A client's own ORDER BY wins,
13875            // which is what MySQL does (MariaDB refuses to let one be
13876            // written at all).
13877            // The synthesised keys have to travel the SAME path a written
13878            // ORDER BY does: the block below is what turns a `grouping()`
13879            // call into the per-branch `__grp_ord_K` column the engine can
13880            // actually sort on. Bypassing it left a bare `grouping(text)`
13881            // for the evaluator to reject.
13882            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13883                self.parse_order_by_keys()?
13884            } else if mysql_rollup {
13885                Self::mysql_rollup_order(&grouping_universe)
13886            } else {
13887                Vec::new()
13888            };
13889            if !synthesised_or_parsed.is_empty() {
13890                let mut order_keys = synthesised_or_parsed;
13891                let mut grp_exprs: Vec<Expr> = Vec::new();
13892                for ob in &order_keys {
13893                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13894                }
13895                for (k, gexpr) in grp_exprs.iter().enumerate() {
13896                    let colname = alloc::format!("__grp_ord_{k}");
13897                    // Head branch (primary set) uses `head_dropped`.
13898                    let mut he = gexpr.clone();
13899                    Self::substitute_grouping_calls(&mut he, &head_dropped);
13900                    stmt.items.push(SelectItem::Expr {
13901                        expr: he,
13902                        alias: Some(colname.clone()),
13903                    });
13904                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13905                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13906                        let set = &grouping_sets[i + 1];
13907                        let dropped: Vec<Expr> = grouping_universe
13908                            .iter()
13909                            .filter(|u| !set.iter().any(|k| k == *u))
13910                            .cloned()
13911                            .collect();
13912                        let mut pe = gexpr.clone();
13913                        Self::substitute_grouping_calls(&mut pe, &dropped);
13914                        peer.items.push(SelectItem::Expr {
13915                            expr: pe,
13916                            alias: Some(colname.clone()),
13917                        });
13918                    }
13919                }
13920                for ob in &mut order_keys {
13921                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13922                }
13923                stmt.order_by = order_keys;
13924            }
13925        }
13926        Ok(stmt)
13927    }
13928
13929    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13930    /// as ORDER BY keys.
13931    ///
13932    /// Per key: the rollup marker, then the key. Sorting on the key alone
13933    /// is not enough, and a table with a NULL in it says why — MariaDB puts
13934    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13935    /// the ROLLUP-introduced NULL last, and both print as NULL.
13936    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13937    /// real group including the data-NULL one, 1 only for the row the
13938    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13939    /// rolls up to NULL|2, a|1, b|3, NULL|6.
13940    ///
13941    /// `#[inline(never)]`: its locals must not join the statement parser's
13942    /// frame, which round 430 measured sitting against the nesting guard.
13943    #[inline(never)]
13944    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13945        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13946        for e in keys {
13947            out.push(OrderBy {
13948                expr: Expr::FunctionCall {
13949                    name: "grouping".into(),
13950                    args: alloc::vec![e.clone()],
13951                },
13952                desc: false,
13953                nulls_first: None,
13954                collation: None,
13955            });
13956            out.push(OrderBy {
13957                expr: e.clone(),
13958                desc: false,
13959                // MySQL orders NULL first on an ascending key.
13960                nulls_first: Some(true),
13961                collation: None,
13962            });
13963        }
13964        out
13965    }
13966
13967    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13968    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13969    #[inline(never)]
13970    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13971        use crate::ast::MaintainKind;
13972        self.skip_paren_option_list();
13973        let kind = match self.peek() {
13974            // `TABLE` and `INDEX` lex as keywords, not identifiers.
13975            Token::Table | Token::Index => {
13976                self.advance();
13977                MaintainKind::ReindexRelation
13978            }
13979            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13980                "index" | "table" => {
13981                    self.advance();
13982                    MaintainKind::ReindexRelation
13983                }
13984                "schema" => {
13985                    self.advance();
13986                    MaintainKind::ReindexSchema
13987                }
13988                "system" | "database" => {
13989                    self.advance();
13990                    MaintainKind::Whole
13991                }
13992                // PG requires the object type; anything else is the
13993                // caller's problem, not something to swallow.
13994                _ => MaintainKind::ReindexRelation,
13995            },
13996            _ => MaintainKind::Whole,
13997        };
13998        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13999        // allows the plain form, so the modifier is recorded rather than
14000        // skipped. It still has no effect on how the reindex runs.
14001        let mut concurrently = false;
14002        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14003            self.advance();
14004            concurrently = true;
14005        }
14006        let target = self.take_optional_maintain_name();
14007        self.consume_until_statement_boundary();
14008        Ok(Statement::Maintain {
14009            kind,
14010            concurrently,
14011            target,
14012        })
14013    }
14014
14015    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14016    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14017    #[inline(never)]
14018    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14019        use crate::ast::MaintainKind;
14020        self.skip_paren_option_list();
14021        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14022            self.advance();
14023        }
14024        let target = self.take_optional_maintain_name();
14025        self.consume_until_statement_boundary();
14026        Ok(Statement::Maintain {
14027            kind: if target.is_some() {
14028                MaintainKind::ClusterRelation
14029            } else {
14030                MaintainKind::Whole
14031            },
14032            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14033            // transaction block quite happily (measured).
14034            concurrently: false,
14035            target,
14036        })
14037    }
14038
14039    /// The next token as a relation / schema name, when there is one.
14040    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14041        match self.peek() {
14042            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14043                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14044                _ => None,
14045            },
14046            _ => None,
14047        }
14048    }
14049
14050    /// A parenthesised option list, absorbed.
14051    fn skip_paren_option_list(&mut self) {
14052        if !matches!(self.peek(), Token::LParen) {
14053            return;
14054        }
14055        let mut depth = 0usize;
14056        loop {
14057            match self.advance() {
14058                Token::LParen => depth += 1,
14059                Token::RParen => {
14060                    depth -= 1;
14061                    if depth == 0 {
14062                        return;
14063                    }
14064                }
14065                Token::Eof => return,
14066                _ => {}
14067            }
14068        }
14069    }
14070
14071    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14072    /// column list.
14073    ///
14074    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14075    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14076    /// / ALL. The three that describe physical storage have no meaning
14077    /// here, so they parse and change nothing rather than making a
14078    /// dump that mentions them fail to load.
14079    ///
14080    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14081    /// parse chain the nesting sentinel is tuned against.
14082    #[inline(never)]
14083    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14084        self.advance(); // LIKE
14085        let source = self.expect_ident_like()?;
14086        let mut options = crate::ast::LikeOptions::default();
14087        loop {
14088            let including = match self.peek() {
14089                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14090                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14091                _ => break,
14092            };
14093            self.advance();
14094            // `ALL` lexes as its own keyword, not an identifier.
14095            let opt = if matches!(self.peek(), Token::All) {
14096                self.advance();
14097                alloc::string::String::from("all")
14098            } else {
14099                self.expect_ident_like()?
14100            };
14101            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14102                o.defaults = on;
14103                o.constraints = on;
14104                o.identity = on;
14105                o.generated = on;
14106                o.indexes = on;
14107                o.comments = on;
14108            };
14109            match opt.to_ascii_lowercase().as_str() {
14110                "all" => set(&mut options, including),
14111                "defaults" => options.defaults = including,
14112                "constraints" => options.constraints = including,
14113                "identity" => options.identity = including,
14114                "generated" => options.generated = including,
14115                "indexes" => options.indexes = including,
14116                "comments" => options.comments = including,
14117                // No storage model to copy into.
14118                "storage" | "statistics" | "compression" => {}
14119                other => {
14120                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14121                }
14122            }
14123        }
14124        Ok(crate::ast::LikeSpec {
14125            source,
14126            at,
14127            options,
14128        })
14129    }
14130
14131    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14132        // Caller already consumed CREATE; we're sitting on TABLE.
14133        debug_assert!(matches!(self.peek(), Token::Table));
14134        self.advance();
14135        let if_not_exists = self.consume_if_not_exists();
14136        let name = self.expect_ident_like()?;
14137        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14138        // child shape has no column list; the child inherits its
14139        // columns from the parent at engine-DDL time. Detect it
14140        // before the `(` requirement below.
14141        if matches!(self.peek(), Token::Partition)
14142            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14143        {
14144            self.advance(); // PARTITION
14145            self.advance(); // of
14146            let partition_of = self.parse_partition_of_tail()?;
14147            return Ok(Statement::CreateTable(CreateTableStatement {
14148                temporary: false,
14149                name,
14150                columns: Vec::new(),
14151                like_specs: Vec::new(),
14152                inherits: Vec::new(),
14153                if_not_exists,
14154                foreign_keys: Vec::new(),
14155                table_constraints: Vec::new(),
14156                partition_by: None,
14157                partition_of: Some(partition_of),
14158            }));
14159        }
14160        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14161        // the materialized-view materialisation path (run the SELECT, infer the
14162        // column types, create + populate the table) but marks the node so the
14163        // executor creates a plain table without a mat-view registry entry.
14164        if matches!(self.peek(), Token::As) {
14165            self.advance();
14166            let body_stmt = self.parse_select_stmt()?;
14167            let Statement::Select(body) = body_stmt else {
14168                return Err(self.err(format!(
14169                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14170                )));
14171            };
14172            let with_data = self.parse_optional_with_data(true)?;
14173            return Ok(Statement::CreateMaterializedView(
14174                crate::ast::CreateMaterializedViewStatement {
14175                    temporary: false,
14176                    name,
14177                    if_not_exists,
14178                    columns: Vec::new(),
14179                    body,
14180                    with_data,
14181                    as_plain_table: true,
14182                },
14183            ));
14184        }
14185        if !matches!(self.peek(), Token::LParen) {
14186            return Err(self.err(format!(
14187                "expected '(' after table name, got {:?}",
14188                self.peek()
14189            )));
14190        }
14191        self.advance();
14192        let mut columns = Vec::new();
14193        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14194        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14195        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14196        loop {
14197            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14198            // column list. It is how a child that adds nothing of its own is
14199            // written, and this loop demanded at least one entry: `syntax
14200            // error at or near ")"`. The child takes the parent's columns,
14201            // which the INHERITS clause already arranges.
14202            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14203                self.advance();
14204                break;
14205            }
14206            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14207            // clauses from column definitions. Constraints start
14208            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14209            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14210            // a column.
14211            if self.peek_table_level_pk_start() {
14212                table_constraints.push(self.parse_table_level_primary_key()?);
14213            } else if matches!(self.peek(), Token::Like) {
14214                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14215                // <opt> ]*`. The source table's shape lives in the catalog,
14216                // so this records the clause and the engine expands it.
14217                like_specs.push(self.parse_create_table_like(columns.len())?);
14218            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14219                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14220                table_constraints.push(self.parse_table_level_exclude()?);
14221            } else if self.peek_table_level_unique_start() {
14222                table_constraints.push(self.parse_table_level_unique()?);
14223            } else if self.peek_table_level_check_start() {
14224                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14225                table_constraints.push(self.parse_table_level_check()?);
14226            } else if self.peek_mysql_inline_key_start() {
14227                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14228                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14229                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14230                // inside the column list. Skip name + paren list;
14231                // for UNIQUE KEY, register as a UC.
14232                if let Some(uc) = self.parse_mysql_inline_key()? {
14233                    table_constraints.push(uc);
14234                }
14235            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14236                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14237                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14238                // CHECK is named, and the named-CONSTRAINT arm used
14239                // to accept FOREIGN KEY only. The name is accepted
14240                // and discarded — same handling as every other SPG
14241                // constraint name.
14242                self.advance(); // CONSTRAINT
14243                // v7.39 (read01 round 48) — the name is kept now: the schema
14244                // stores it, so DROP / RENAME CONSTRAINT can find it.
14245                let con_name = self.expect_ident_like()?;
14246                let mut tc = match kind {
14247                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14248                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14249                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14250                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14251                };
14252                match &mut tc {
14253                    crate::ast::TableConstraint::Check { name, .. }
14254                    | crate::ast::TableConstraint::Unique { name, .. }
14255                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14256                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14257                        *name = Some(con_name);
14258                    }
14259                    _ => {}
14260                }
14261                table_constraints.push(tc);
14262            } else if self.peek_constraint_or_fk_start() {
14263                foreign_keys.push(self.parse_table_level_fk()?);
14264            } else {
14265                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14266                // v7.13.0 — fold inline UNIQUE / CHECK column
14267                // constraints into table-level entries so the
14268                // engine path stays uniform.
14269                if col.is_unique {
14270                    table_constraints.push(crate::ast::TableConstraint::Unique {
14271                        name: None,
14272                        columns: alloc::vec![col.name.clone()],
14273                        nulls_not_distinct: col.unique_nulls_not_distinct,
14274                        deferrable: col.constraint_deferrable,
14275                        initially_deferred: col.constraint_initially_deferred,
14276                    });
14277                }
14278                if let Some(check_expr) = col.check.clone() {
14279                    table_constraints.push(crate::ast::TableConstraint::Check {
14280                        name: None,
14281                        expr: check_expr,
14282                        not_valid: false,
14283                    });
14284                }
14285                columns.push(col);
14286                if let Some(fk) = col_level_fk {
14287                    foreign_keys.push(fk);
14288                }
14289            }
14290            match self.peek() {
14291                Token::Comma => {
14292                    self.advance();
14293                }
14294                Token::RParen => {
14295                    self.advance();
14296                    break;
14297                }
14298                other => {
14299                    return Err(
14300                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14301                    );
14302                }
14303            }
14304        }
14305        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14306        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14307        // nothing is written between the parentheses.
14308        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14309        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14310        // empty parentheses were a parse error in their own right — quite apart
14311        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14312        // SPG does not have (filed separately).
14313        let _ = &like_specs;
14314        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14315        // It sits between the column list and the MySQL table options,
14316        // and it was a syntax error until this round.
14317        let mut inherits: Vec<String> = Vec::new();
14318        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14319            if k.eq_ignore_ascii_case("inherits"))
14320        {
14321            self.advance();
14322            if !matches!(self.peek(), Token::LParen) {
14323                return Err(self.err(alloc::format!(
14324                    "expected ( after INHERITS, got {:?}",
14325                    self.peek()
14326                )));
14327            }
14328            self.advance();
14329            loop {
14330                inherits.push(self.expect_ident_like()?);
14331                if matches!(self.peek(), Token::Comma) {
14332                    self.advance();
14333                    continue;
14334                }
14335                break;
14336            }
14337            if !matches!(self.peek(), Token::RParen) {
14338                return Err(self.err(alloc::format!(
14339                    "expected ) closing INHERITS, got {:?}",
14340                    self.peek()
14341                )));
14342            }
14343            self.advance();
14344        }
14345        // v7.14.0 — consume MySQL/MariaDB table options after the
14346        // closing `)`. mysqldump emits things like
14347        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14348        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14349        // SPG accepts all forms as no-ops (each option is
14350        // `<ident> [=] <ident-or-string>` separated by whitespace).
14351        self.consume_mysql_table_options();
14352        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14353        // SPG has no per-table reloptions, so accept and ignore them so a
14354        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14355        self.consume_with_reloptions();
14356        // v7.37.6-B — declarative-partition-parent suffix
14357        // (`PARTITION BY RANGE (key_col)`) sits after the column
14358        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14359        // and locks the key column at one ident; the engine then
14360        // verifies the column type is TIMESTAMPTZ.
14361        let partition_by = if matches!(self.peek(), Token::Partition) {
14362            self.advance(); // PARTITION
14363            if !self.peek_is_by() {
14364                return Err(self.err(format!(
14365                    "expected BY after PARTITION, got {:?}",
14366                    self.peek()
14367                )));
14368            }
14369            self.advance();
14370            Some(self.parse_partition_by_tail()?)
14371        } else {
14372            None
14373        };
14374        Ok(Statement::CreateTable(CreateTableStatement {
14375            temporary: false,
14376            name,
14377            columns,
14378            like_specs,
14379            inherits,
14380            if_not_exists,
14381            foreign_keys,
14382            table_constraints,
14383            partition_by,
14384            partition_of: None,
14385        }))
14386    }
14387
14388    /// v7.37.6-B — case-insensitive ident match helper for the
14389    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14390    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14391    /// didn't burn a global keyword slot for each (see the
14392    /// `Token::Partition` doc-comment in `lexer.rs`).
14393    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14394        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14395    }
14396
14397    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14398    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14399    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14400        use crate::ast::{PartitionBySpec, PartitionKindAst};
14401        let kind = match self.peek() {
14402            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14403                self.advance();
14404                PartitionKindAst::Range
14405            }
14406            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14407                self.advance();
14408                PartitionKindAst::List
14409            }
14410            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14411                self.advance();
14412                PartitionKindAst::Hash
14413            }
14414            other => {
14415                return Err(self.err(format!(
14416                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14417                )));
14418            }
14419        };
14420        if !matches!(self.peek(), Token::LParen) {
14421            return Err(self.err(format!(
14422                "expected '(' after PARTITION BY <strategy>, got {:?}",
14423                self.peek()
14424            )));
14425        }
14426        self.advance();
14427        let mut key_columns = Vec::new();
14428        loop {
14429            key_columns.push(self.expect_ident_like()?);
14430            match self.peek() {
14431                Token::Comma => {
14432                    self.advance();
14433                }
14434                Token::RParen => {
14435                    self.advance();
14436                    break;
14437                }
14438                other => {
14439                    return Err(self.err(format!(
14440                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14441                    )));
14442                }
14443            }
14444        }
14445        if key_columns.is_empty() {
14446            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14447        }
14448        Ok(PartitionBySpec { kind, key_columns })
14449    }
14450
14451    /// v7.37.6-B — after `PARTITION OF`, expect
14452    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14453    /// or
14454    ///   <parent> DEFAULT
14455    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14456        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14457        let parent_name = self.expect_ident_like()?;
14458        // v7.37.6-B rejects an explicit column list — the child
14459        // inherits from the parent. mailrs round-7 taught us that
14460        // CREATE TABLE-side schema reconciliation hides drift, so
14461        // we surface this as a parse error rather than silently
14462        // ignoring user columns.
14463        if matches!(self.peek(), Token::LParen) {
14464            return Err(self.err(
14465                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14466                 at v7.37.6-B; the child inherits its columns from the parent"
14467                    .to_string(),
14468            ));
14469        }
14470        let bounds = match self.peek() {
14471            Token::Default => {
14472                self.advance();
14473                PartitionOfBoundsAst::Default
14474            }
14475            Token::For => {
14476                self.advance();
14477                if !matches!(self.peek(), Token::Values) {
14478                    return Err(
14479                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14480                    );
14481                }
14482                self.advance();
14483                // WITH is not a reserved Token in the lexer — it lexes
14484                // as Token::Ident("with"). Disambiguate manually.
14485                let want_with = matches!(
14486                    self.peek(),
14487                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14488                );
14489                if want_with {
14490                    self.advance();
14491                    if !matches!(self.peek(), Token::LParen) {
14492                        return Err(self.err(format!(
14493                            "expected '(' after FOR VALUES WITH, got {:?}",
14494                            self.peek()
14495                        )));
14496                    }
14497                    self.advance();
14498                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14499                    loop {
14500                        let key = self.expect_ident_like()?;
14501                        let n = match self.peek().clone() {
14502                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14503                                self.advance();
14504                                v as u32
14505                            }
14506                            other => {
14507                                return Err(self.err(format!(
14508                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14509                                )));
14510                            }
14511                        };
14512                        match key.to_ascii_uppercase().as_str() {
14513                            "MODULUS" => modulus = Some(n),
14514                            "REMAINDER" => remainder = Some(n),
14515                            other => {
14516                                return Err(self.err(format!(
14517                                    "FOR VALUES WITH: unknown key {other:?}; \
14518                                     expected MODULUS or REMAINDER"
14519                                )));
14520                            }
14521                        }
14522                        match self.peek() {
14523                            Token::Comma => {
14524                                self.advance();
14525                            }
14526                            Token::RParen => {
14527                                self.advance();
14528                                break;
14529                            }
14530                            other => {
14531                                return Err(self.err(format!(
14532                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14533                                )));
14534                            }
14535                        }
14536                    }
14537                    let modulus = modulus
14538                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14539                    let remainder = remainder.ok_or_else(|| {
14540                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14541                    })?;
14542                    if modulus == 0 {
14543                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14544                    }
14545                    if remainder >= modulus {
14546                        return Err(self.err(format!(
14547                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14548                             must be < MODULUS ({modulus})"
14549                        )));
14550                    }
14551                    PartitionOfBoundsAst::Hash { modulus, remainder }
14552                } else {
14553                    match self.peek() {
14554                        Token::From => {
14555                            self.advance();
14556                            let lower = Box::new(self.parse_partition_bound_expr()?);
14557                            if !matches!(self.peek(), Token::To) {
14558                                return Err(self.err(format!(
14559                                    "expected TO after FROM (...), got {:?}",
14560                                    self.peek()
14561                                )));
14562                            }
14563                            self.advance();
14564                            let upper = Box::new(self.parse_partition_bound_expr()?);
14565                            PartitionOfBoundsAst::Range { lower, upper }
14566                        }
14567                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14568                        Token::In => {
14569                            self.advance();
14570                            if !matches!(self.peek(), Token::LParen) {
14571                                return Err(self.err(format!(
14572                                    "expected '(' after FOR VALUES IN, got {:?}",
14573                                    self.peek()
14574                                )));
14575                            }
14576                            self.advance();
14577                            let mut values = Vec::new();
14578                            loop {
14579                                values.push(self.parse_expr(0)?);
14580                                match self.peek() {
14581                                    Token::Comma => {
14582                                        self.advance();
14583                                    }
14584                                    Token::RParen => {
14585                                        self.advance();
14586                                        break;
14587                                    }
14588                                    other => {
14589                                        return Err(self.err(format!(
14590                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14591                                    )));
14592                                    }
14593                                }
14594                            }
14595                            if values.is_empty() {
14596                                return Err(self.err(
14597                                    "FOR VALUES IN requires at least one literal".to_string(),
14598                                ));
14599                            }
14600                            PartitionOfBoundsAst::List { values }
14601                        }
14602                        other => {
14603                            return Err(self.err(format!(
14604                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14605                            )));
14606                        }
14607                    }
14608                }
14609            }
14610            other => {
14611                return Err(self.err(format!(
14612                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14613                )));
14614            }
14615        };
14616        Ok(PartitionOfSpec {
14617            parent_name,
14618            bounds,
14619        })
14620    }
14621
14622    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14623    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14624    /// markers (no-arg builtins) so the engine resolves them
14625    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14626    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14627        if !matches!(self.peek(), Token::LParen) {
14628            return Err(self.err(format!(
14629                "expected '(' before partition bound, got {:?}",
14630                self.peek()
14631            )));
14632        }
14633        self.advance();
14634        let expr = match self.peek() {
14635            Token::Ident(s) | Token::QuotedIdent(s)
14636                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14637            {
14638                let name = s.to_ascii_uppercase();
14639                self.advance();
14640                crate::ast::Expr::FunctionCall {
14641                    name,
14642                    args: Vec::new(),
14643                }
14644            }
14645            _ => self.parse_expr(0)?,
14646        };
14647        if !matches!(self.peek(), Token::RParen) {
14648            return Err(self.err(format!(
14649                "expected ')' after partition bound, got {:?}",
14650                self.peek()
14651            )));
14652        }
14653        self.advance();
14654        Ok(expr)
14655    }
14656
14657    /// v7.14.0 — true when the next tokens look like an inline
14658    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14659    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14660    /// — each followed by an optional name + `(...)`. Critical:
14661    /// a column NAMED `key` / `index` (PG accepts as ident) must
14662    /// NOT be mistaken for the KEY constraint shape. We disambig
14663    /// by requiring the keyword to be followed by either `(` or
14664    /// `<ident> (`.
14665    fn peek_mysql_inline_key_start(&self) -> bool {
14666        let cur = self.peek();
14667        // Shapes:
14668        //   KEY (cols)
14669        //   KEY name (cols)
14670        //   INDEX (cols)
14671        //   INDEX name (cols)
14672        //   UNIQUE KEY [name] (cols)
14673        //   UNIQUE INDEX [name] (cols)
14674        //   FULLTEXT [KEY|INDEX] [name] (cols)
14675        //   SPATIAL [KEY|INDEX] [name] (cols)
14676        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14677            // tokens at skip = the position AFTER the index-form
14678            // keywords (KEY/INDEX) have been consumed.
14679            match self.tokens.get(skip) {
14680                Some(Token::LParen) => true,
14681                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14682                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14683                }
14684                _ => false,
14685            }
14686        };
14687        // `INDEX` lexes as Token::Index (reserved), not as
14688        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14689        // start; the peek helper below handles either.
14690        let is_key_or_index_tok = |t: &Token| -> bool {
14691            matches!(t, Token::Index)
14692                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14693        };
14694        match cur {
14695            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14696            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14697                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14698            }
14699            Token::Ident(s)
14700                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14701            {
14702                let nxt = self.tokens.get(self.pos + 1);
14703                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14704                    self.pos + 2
14705                } else {
14706                    self.pos + 1
14707                };
14708                after_keyword_followed_by_paren_or_ident_paren(after_after)
14709            }
14710            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14711                let nxt = self.tokens.get(self.pos + 1);
14712                if !nxt.is_some_and(is_key_or_index_tok) {
14713                    return false;
14714                }
14715                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14716            }
14717            _ => false,
14718        }
14719    }
14720
14721    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14722    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14723    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14724    /// returns Some(TableConstraint::Index) so the engine builds
14725    /// a real BTree index on the leading column (mysqldump
14726    /// `KEY idx_posts_author (author_id)` shape).
14727    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14728    /// (the storage layer has no matching AM).
14729    fn parse_mysql_inline_key(
14730        &mut self,
14731    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14732        // Detect UNIQUE prefix.
14733        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14734        {
14735            self.advance();
14736            true
14737        } else {
14738            false
14739        };
14740        // Consume FULLTEXT / SPATIAL prefix and record which one
14741        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14742        // dedicated TableConstraint variant so the engine can
14743        // build a tsvector-GIN; SPATIAL still has no matching
14744        // AM, so it falls back to accept-as-no-op.
14745        let mut is_fulltext = false;
14746        let mut is_spatial = false;
14747        if let Token::Ident(s) = self.peek().clone() {
14748            if s.eq_ignore_ascii_case("fulltext") {
14749                self.advance();
14750                is_fulltext = true;
14751            } else if s.eq_ignore_ascii_case("spatial") {
14752                self.advance();
14753                is_spatial = true;
14754            }
14755        }
14756        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14757        // (reserved); accept either token shape.
14758        match self.peek() {
14759            Token::Index => {
14760                self.advance();
14761            }
14762            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14763                self.advance();
14764            }
14765            other => {
14766                return Err(self.err(alloc::format!(
14767                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14768                )));
14769            }
14770        }
14771        // Optional index name (an ident before the `(`).
14772        // v7.15.0 — capture the name when present so the engine
14773        // builds the secondary index under the user's chosen
14774        // name (matches mysqldump's `KEY idx_x (col)` shape).
14775        let mut idx_name: Option<String> = None;
14776        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14777            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14778        {
14779            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14780                idx_name = Some(s);
14781            }
14782        }
14783        // Optional `USING BTREE` / `USING HASH` (MySQL).
14784        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14785            self.advance();
14786            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14787                self.advance();
14788            }
14789        }
14790        // Required column list `(col [, col]*)`.
14791        if !matches!(self.peek(), Token::LParen) {
14792            return Err(self.err(alloc::format!(
14793                "expected '(' in inline KEY/INDEX, got {:?}",
14794                self.peek()
14795            )));
14796        }
14797        self.advance();
14798        let mut cols: Vec<String> = Vec::new();
14799        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14800            self.advance();
14801            cols.push(s);
14802            // Skip optional `(length)` per-column prefix.
14803            if matches!(self.peek(), Token::LParen) {
14804                let mut depth = 1usize;
14805                self.advance();
14806                while depth > 0 {
14807                    match self.peek() {
14808                        Token::LParen => depth += 1,
14809                        Token::RParen => depth -= 1,
14810                        Token::Eof => break,
14811                        _ => {}
14812                    }
14813                    self.advance();
14814                }
14815            }
14816            // Skip optional ASC / DESC.
14817            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14818                || matches!(self.peek(), Token::Asc | Token::Desc)
14819            {
14820                self.advance();
14821            }
14822            if matches!(self.peek(), Token::Comma) {
14823                self.advance();
14824                continue;
14825            }
14826            break;
14827        }
14828        if matches!(self.peek(), Token::RParen) {
14829            self.advance();
14830        }
14831        // Trailing options on the inline index — comment / etc.
14832        // Skip until comma or `)`.
14833        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14834            self.advance();
14835        }
14836        if cols.is_empty() {
14837            return Ok(None);
14838        }
14839        if is_unique {
14840            // Carry the captured idx_name on UNIQUE too so future
14841            // engine work can name the underlying BTree
14842            // accordingly; today the unique-constraint installer
14843            // synthesises the name itself, but Display round-trip
14844            // benefits from preserving it.
14845            Ok(Some(crate::ast::TableConstraint::Unique {
14846                name: idx_name,
14847                columns: cols,
14848                nulls_not_distinct: false,
14849                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14850                deferrable: false,
14851                initially_deferred: false,
14852            }))
14853        } else if is_fulltext {
14854            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14855            // routes through `TableConstraint::FulltextIndex`;
14856            // the engine builds a tsvector-GIN over each named
14857            // column so MATCH AGAINST gets a real inverted
14858            // index instead of a silently-dropped declaration.
14859            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14860                name: idx_name,
14861                columns: cols,
14862            }))
14863        } else if is_spatial {
14864            // SPG has no native SPATIAL AM. Accept-as-no-op
14865            // (declaration is parsed, but no index is built).
14866            Ok(None)
14867        } else {
14868            // v7.15.0 — plain KEY / INDEX builds a real BTree
14869            // secondary index.
14870            Ok(Some(crate::ast::TableConstraint::Index {
14871                name: idx_name,
14872                columns: cols,
14873            }))
14874        }
14875    }
14876
14877    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14878    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14879    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14880    /// (in any order, separated by whitespace).
14881    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14882    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14883    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14884    /// bare ident here, and only the parenthesised form is reloptions (so this
14885    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14886    fn consume_with_reloptions(&mut self) {
14887        let is_with = matches!(
14888            self.peek(),
14889            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14890        );
14891        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14892            return;
14893        }
14894        self.advance(); // WITH
14895        self.advance(); // (
14896        let mut depth = 1u32;
14897        while depth > 0 && !matches!(self.peek(), Token::Eof) {
14898            match self.peek() {
14899                Token::LParen => depth += 1,
14900                Token::RParen => depth -= 1,
14901                _ => {}
14902            }
14903            self.advance();
14904        }
14905    }
14906
14907    fn consume_mysql_table_options(&mut self) {
14908        loop {
14909            // Heuristic: a table option is an ident (or `DEFAULT`
14910            // reserved keyword) followed by `=` and an
14911            // ident / string / integer.
14912            let name_lc = match self.peek().clone() {
14913                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14914                Token::Default => alloc::string::String::from("default"),
14915                _ => break,
14916            };
14917            let known = matches!(
14918                name_lc.as_str(),
14919                "engine"
14920                    | "default"
14921                    | "charset"
14922                    | "collate"
14923                    | "auto_increment"
14924                    | "row_format"
14925                    | "comment"
14926                    | "pack_keys"
14927                    | "stats_persistent"
14928                    | "stats_auto_recalc"
14929                    | "stats_sample_pages"
14930                    | "key_block_size"
14931                    | "tablespace"
14932                    | "min_rows"
14933                    | "max_rows"
14934                    | "checksum"
14935                    | "delay_key_write"
14936                    | "insert_method"
14937                    | "data"
14938                    | "index"
14939                    | "encryption"
14940                    | "compression"
14941            );
14942            if !known {
14943                break;
14944            }
14945            self.advance(); // option name
14946            // `DEFAULT` optional prefix is followed by `CHARSET` /
14947            // `COLLATE`; consume the next ident too.
14948            if name_lc == "default" {
14949                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14950                    self.advance();
14951                }
14952            }
14953            if matches!(self.peek(), Token::Eq) {
14954                self.advance();
14955            }
14956            match self.peek() {
14957                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14958                    self.advance();
14959                }
14960                _ => {}
14961            }
14962        }
14963    }
14964
14965    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14966    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14967    /// sure (otherwise a column literally named `primary` would
14968    /// be mistaken).
14969    fn peek_table_level_pk_start(&self) -> bool {
14970        let cur = self.peek();
14971        let nxt = self.tokens.get(self.pos + 1);
14972        let nxt2 = self.tokens.get(self.pos + 2);
14973        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14974        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14975        let is_lparen = matches!(nxt2, Some(Token::LParen));
14976        is_primary && is_key && is_lparen
14977    }
14978
14979    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14980    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14981    /// (mailrs round-5 G10).
14982    fn peek_table_level_unique_start(&self) -> bool {
14983        let cur = self.peek();
14984        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14985        if !is_unique {
14986            return false;
14987        }
14988        let n1 = self.tokens.get(self.pos + 1);
14989        // Plain `UNIQUE (…)`.
14990        if matches!(n1, Some(Token::LParen)) {
14991            return true;
14992        }
14993        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14994        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14995        if !is_nulls {
14996            return false;
14997        }
14998        let n2 = self.tokens.get(self.pos + 2);
14999        let n3 = self.tokens.get(self.pos + 3);
15000        let n4 = self.tokens.get(self.pos + 4);
15001        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15002        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15003            return true;
15004        }
15005        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15006        if matches!(n2, Some(Token::Not))
15007            && matches!(n3, Some(Token::Distinct))
15008            && matches!(n4, Some(Token::LParen))
15009        {
15010            return true;
15011        }
15012        false
15013    }
15014
15015    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15016        self.advance(); // PRIMARY
15017        self.advance(); // KEY
15018        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15019        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15020        // 621 consumed and dropped them (the storing half of F08).
15021        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15022        Ok(crate::ast::TableConstraint::PrimaryKey {
15023            name: None,
15024            columns,
15025            deferrable,
15026            initially_deferred,
15027        })
15028    }
15029
15030    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15031        self.advance(); // UNIQUE
15032        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15033        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15034        // is `NULLS DISTINCT` per the SQL standard.
15035        let mut nulls_not_distinct = false;
15036        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15037            let n1 = self.tokens.get(self.pos + 1);
15038            let n2 = self.tokens.get(self.pos + 2);
15039            let is_not = matches!(n1, Some(Token::Not));
15040            let is_distinct = matches!(n2, Some(Token::Distinct));
15041            if is_not && is_distinct {
15042                self.advance(); // NULLS
15043                self.advance(); // NOT
15044                self.advance(); // DISTINCT
15045                nulls_not_distinct = true;
15046            } else if matches!(n1, Some(Token::Distinct)) {
15047                self.advance(); // NULLS
15048                self.advance(); // DISTINCT
15049            }
15050        }
15051        let columns = self.parse_paren_ident_list("UNIQUE")?;
15052        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15053        Ok(crate::ast::TableConstraint::Unique {
15054            name: None,
15055            columns,
15056            nulls_not_distinct,
15057            deferrable,
15058            initially_deferred,
15059        })
15060    }
15061
15062    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15063    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15064    /// expression.
15065    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15066    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15067    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15068    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15069    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15070    /// commit: `NOT` starts no other suffix here, but reading both
15071    /// tokens before advancing keeps the caller's error message intact
15072    /// if someone writes `NOT NULL` by mistake.
15073    fn parse_not_valid_suffix(&mut self) -> bool {
15074        if !matches!(self.peek(), Token::Not) {
15075            return false;
15076        }
15077        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15078        {
15079            return false;
15080        }
15081        self.advance();
15082        self.advance();
15083        true
15084    }
15085
15086    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15087        self.advance(); // EXCLUDE
15088        // Optional `USING <method>`.
15089        let mut method = None;
15090        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15091            self.advance();
15092            method = Some(match self.advance() {
15093                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15094                other => {
15095                    return Err(self.err(alloc::format!(
15096                        "expected index method after USING, got {other:?}"
15097                    )));
15098                }
15099            });
15100        }
15101        if !matches!(self.peek(), Token::LParen) {
15102            return Err(self.err(alloc::format!(
15103                "expected '(' after EXCLUDE, got {:?}",
15104                self.peek()
15105            )));
15106        }
15107        self.advance();
15108        let mut elements: Vec<(String, String)> = Vec::new();
15109        loop {
15110            let col = match self.advance() {
15111                Token::Ident(s) | Token::QuotedIdent(s) => s,
15112                other => {
15113                    return Err(self.err(alloc::format!(
15114                        "expected column name in EXCLUDE, got {other:?}"
15115                    )));
15116                }
15117            };
15118            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15119                return Err(self.err(alloc::format!(
15120                    "expected WITH after EXCLUDE column, got {:?}",
15121                    self.peek()
15122                )));
15123            }
15124            self.advance();
15125            let op = match self.advance() {
15126                Token::InetOverlap => String::from("&&"),
15127                Token::Intersects => String::from("?#"),
15128                Token::IsBelow => String::from("<^"),
15129                Token::IsAbove => String::from(">^"),
15130                Token::PatternLt => String::from("~<~"),
15131                Token::PatternLtEq => String::from("~<=~"),
15132                Token::PatternGt => String::from("~>~"),
15133                Token::PatternGtEq => String::from("~>=~"),
15134                Token::TsMatchOld => String::from("@@@"),
15135                Token::Eq => String::from("="),
15136                Token::JsonContains => String::from("@>"),
15137                Token::JsonContainedBy => String::from("<@"),
15138                Token::OverLeft => String::from("&<"),
15139                Token::OverRight => String::from("&>"),
15140                other => {
15141                    return Err(self.err(alloc::format!(
15142                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15143                    )));
15144                }
15145            };
15146            elements.push((col, op));
15147            if matches!(self.peek(), Token::Comma) {
15148                self.advance();
15149                continue;
15150            }
15151            break;
15152        }
15153        if !matches!(self.peek(), Token::RParen) {
15154            return Err(self.err(alloc::format!(
15155                "expected ')' to close EXCLUDE, got {:?}",
15156                self.peek()
15157            )));
15158        }
15159        self.advance();
15160        Ok(crate::ast::TableConstraint::Exclude {
15161            name: None,
15162            method,
15163            elements,
15164        })
15165    }
15166
15167    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15168        self.advance(); // CHECK
15169        if !matches!(self.peek(), Token::LParen) {
15170            return Err(self.err(alloc::format!(
15171                "expected '(' after CHECK, got {:?}",
15172                self.peek()
15173            )));
15174        }
15175        self.advance();
15176        let expr = self.parse_expr(0)?;
15177        if !matches!(self.peek(), Token::RParen) {
15178            return Err(self.err(alloc::format!(
15179                "expected ')' to close CHECK predicate, got {:?}",
15180                self.peek()
15181            )));
15182        }
15183        self.advance();
15184        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15185        // are no existing rows for PG to skip, so it rejects the suffix.
15186        Ok(crate::ast::TableConstraint::Check {
15187            name: None,
15188            expr,
15189            not_valid: false,
15190        })
15191    }
15192
15193    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15194    fn peek_table_level_check_start(&self) -> bool {
15195        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15196    }
15197
15198    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15199    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15200    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15201    /// own CONSTRAINT prefix).
15202    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15203        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15204            return None;
15205        }
15206        // tokens[pos+1] is the constraint name (any ident-like);
15207        // tokens[pos+2] is the kind keyword.
15208        match self.tokens.get(self.pos + 2) {
15209            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15210                Some(NamedTableConstraintKind::Check)
15211            }
15212            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15213                Some(NamedTableConstraintKind::Unique)
15214            }
15215            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15216                Some(NamedTableConstraintKind::PrimaryKey)
15217            }
15218            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15219                Some(NamedTableConstraintKind::Exclude)
15220            }
15221            _ => None,
15222        }
15223    }
15224
15225    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15226        if !matches!(self.peek(), Token::LParen) {
15227            return Err(self.err(alloc::format!(
15228                "expected '(' after {ctx}, got {:?}",
15229                self.peek()
15230            )));
15231        }
15232        self.advance();
15233        let mut out = Vec::new();
15234        loop {
15235            out.push(self.expect_ident_like()?);
15236            match self.peek() {
15237                Token::Comma => {
15238                    self.advance();
15239                }
15240                Token::RParen => {
15241                    self.advance();
15242                    break;
15243                }
15244                other => {
15245                    return Err(self.err(alloc::format!(
15246                        "expected ',' or ')' in {ctx} list, got {other:?}"
15247                    )));
15248                }
15249            }
15250        }
15251        if out.is_empty() {
15252            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15253        }
15254        Ok(out)
15255    }
15256
15257    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15258    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15259    /// table-level FK; a column def never starts with either keyword
15260    /// (column names are not in this reserved set).
15261    fn peek_constraint_or_fk_start(&self) -> bool {
15262        let is_constraint_kw = matches!(
15263            self.peek(),
15264            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15265        );
15266        let is_foreign_kw = matches!(
15267            self.peek(),
15268            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15269        );
15270        is_constraint_kw || is_foreign_kw
15271    }
15272
15273    /// v7.6.0 — parse a table-level FK clause:
15274    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15275    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15276    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15277        let mut name: Option<String> = None;
15278        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15279            self.advance();
15280            name = Some(self.expect_ident_like()?);
15281        }
15282        // `FOREIGN`
15283        match self.advance() {
15284            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15285            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15286        }
15287        // `KEY`
15288        match self.advance() {
15289            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15290            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15291        }
15292        // `(col, col, ...)`
15293        if !matches!(self.peek(), Token::LParen) {
15294            return Err(self.err(format!(
15295                "expected '(' after FOREIGN KEY, got {:?}",
15296                self.peek()
15297            )));
15298        }
15299        self.advance();
15300        let mut columns = Vec::new();
15301        loop {
15302            columns.push(self.expect_ident_like()?);
15303            match self.peek() {
15304                Token::Comma => {
15305                    self.advance();
15306                }
15307                Token::RParen => {
15308                    self.advance();
15309                    break;
15310                }
15311                other => {
15312                    return Err(self.err(format!(
15313                        "expected ',' or ')' in FK column list, got {other:?}"
15314                    )));
15315                }
15316            }
15317        }
15318        if columns.is_empty() {
15319            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15320        }
15321        let (
15322            parent_table,
15323            parent_columns,
15324            on_delete,
15325            on_update,
15326            match_type,
15327            deferrable,
15328            initially_deferred,
15329        ) = self.parse_references_tail(columns.len())?;
15330        Ok(ForeignKeyConstraint {
15331            name,
15332            columns,
15333            parent_table,
15334            parent_columns,
15335            on_delete,
15336            on_update,
15337            match_type,
15338            deferrable,
15339            initially_deferred,
15340        })
15341    }
15342
15343    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15344    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15345    /// the local column count, used to default the parent column
15346    /// list when omitted (SQL spec: parent's PK is implied).
15347    fn parse_references_tail(
15348        &mut self,
15349        expected_arity: usize,
15350    ) -> Result<
15351        (
15352            String,
15353            Vec<String>,
15354            FkAction,
15355            FkAction,
15356            crate::ast::MatchType,
15357            // v7.39 (round 288) — deferrable, initially_deferred.
15358            bool,
15359            bool,
15360        ),
15361        ParseError,
15362    > {
15363        match self.advance() {
15364            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15365            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15366        }
15367        let parent_table = self.expect_ident_like()?;
15368        let mut parent_columns: Vec<String> = Vec::new();
15369        if matches!(self.peek(), Token::LParen) {
15370            self.advance();
15371            loop {
15372                parent_columns.push(self.expect_ident_like()?);
15373                match self.peek() {
15374                    Token::Comma => {
15375                        self.advance();
15376                    }
15377                    Token::RParen => {
15378                        self.advance();
15379                        break;
15380                    }
15381                    other => {
15382                        return Err(self.err(format!(
15383                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15384                        )));
15385                    }
15386                }
15387            }
15388        }
15389        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15390            return Err(self.err(format!(
15391                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15392                expected_arity,
15393                parent_columns.len()
15394            )));
15395        }
15396        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15397        // it between the referenced column list and the ON / DEFERRABLE
15398        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15399        // is skipped when any referencing column is NULL), so SIMPLE —
15400        // the default, and the only spelling pg_dump emits — is accepted
15401        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15402        // mixed-NULL rule, which is not wired yet; reject them honestly
15403        // rather than silently applying SIMPLE (PG itself errors on
15404        // MATCH PARTIAL as "not yet implemented").
15405        let mut match_type = crate::ast::MatchType::Simple;
15406        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15407            self.advance();
15408            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15409            // SIMPLE / PARTIAL arrive as bare identifiers.
15410            let kind = match self.advance() {
15411                Token::Full => "FULL".to_string(),
15412                Token::Ident(s) => s.to_uppercase(),
15413                other => {
15414                    return Err(self.err(format!(
15415                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15416                    )));
15417                }
15418            };
15419            match kind.as_str() {
15420                "SIMPLE" => {} // Default — match_type stays Simple.
15421                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15422                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15423                "FULL" => match_type = crate::ast::MatchType::Full,
15424                "PARTIAL" => {
15425                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15426                }
15427                _ => {
15428                    return Err(self.err(format!(
15429                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15430                    )));
15431                }
15432            }
15433        }
15434        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15435        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15436        // <action>` / `ON UPDATE <action>` in either order. PG /
15437        // pg_dump emits the timing clause AFTER the ON clauses
15438        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15439        // but the SQL spec allows either order. We loop over
15440        // every possible trailer and dispatch on the next token,
15441        // stopping when nothing matches. Phase 3.1 changes the
15442        // bare DEFERRABLE form from hard-error to accept-as-
15443        // immediate; SPG is single-writer with no deferred-
15444        // constraint window so the runtime semantics are always
15445        // immediate even when INITIALLY DEFERRED is requested.
15446        // PG's default referential action (no ON DELETE / ON UPDATE
15447        // clause) is NO ACTION, not RESTRICT — the two enforce
15448        // identically in SPG (single-writer, no deferred window; see the
15449        // shared match arm in constraints.rs) but information_schema.
15450        // referential_constraints must report NO ACTION to match PG.
15451        let mut on_delete = FkAction::NoAction;
15452        let mut on_update = FkAction::NoAction;
15453        let mut seen_on_delete = false;
15454        let mut seen_on_update = false;
15455        let mut deferrable = false;
15456        let mut initially_deferred = false;
15457        loop {
15458            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15459            let before = self.pos;
15460            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15461            if self.pos != before {
15462                deferrable = d;
15463                initially_deferred = idef;
15464                continue;
15465            }
15466            // ON DELETE / ON UPDATE.
15467            if !matches!(self.peek(), Token::On) {
15468                break;
15469            }
15470            self.advance();
15471            let which = self.advance();
15472            let action = self.parse_fk_action()?;
15473            match which {
15474                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15475                    if seen_on_delete {
15476                        return Err(self.err("ON DELETE specified twice".into()));
15477                    }
15478                    seen_on_delete = true;
15479                    on_delete = action;
15480                }
15481                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15482                    if seen_on_update {
15483                        return Err(self.err("ON UPDATE specified twice".into()));
15484                    }
15485                    seen_on_update = true;
15486                    on_update = action;
15487                }
15488                other => {
15489                    return Err(
15490                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15491                    );
15492                }
15493            }
15494        }
15495        Ok((
15496            parent_table,
15497            parent_columns,
15498            on_delete,
15499            on_update,
15500            match_type,
15501            deferrable,
15502            initially_deferred,
15503        ))
15504    }
15505
15506    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15507    /// NO ACTION`.
15508    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15509        match self.advance() {
15510            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15511            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15512            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15513                Token::Null => Ok(FkAction::SetNull),
15514                Token::Default => Ok(FkAction::SetDefault),
15515                other => Err(self.err(format!(
15516                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15517                ))),
15518            },
15519            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15520                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15521                other => Err(self.err(format!(
15522                    "expected ACTION after NO in FK action, got {other:?}"
15523                ))),
15524            },
15525            other => Err(self.err(format!(
15526                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15527            ))),
15528        }
15529    }
15530
15531    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15532    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15533    fn consume_if_not_exists(&mut self) -> bool {
15534        // `IF` arrives as a bare Ident (we don't reserve it because it
15535        // also appears mid-expression in PG, though we don't support
15536        // those forms yet).
15537        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15538        if !looks_like_if {
15539            return false;
15540        }
15541        // Peek one ahead before committing: only consume IF when it's
15542        // actually `IF NOT EXISTS`.
15543        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15544            return false;
15545        }
15546        if !matches!(
15547            self.tokens.get(self.pos + 2),
15548            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15549        ) {
15550            return false;
15551        }
15552        self.advance(); // IF
15553        self.advance(); // NOT
15554        self.advance(); // EXISTS
15555        true
15556    }
15557
15558    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15559    /// Consumes IF EXISTS as a pair; returns false otherwise
15560    /// without consuming any tokens.
15561    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15562    /// ENABLE/DISABLE/FORCE/NO FORCE.
15563    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15564        for kw in ["row", "level", "security"] {
15565            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15566            {
15567                return Err(self.err(alloc::format!(
15568                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15569                    kw.to_ascii_uppercase(),
15570                    self.peek()
15571                )));
15572            }
15573            self.advance();
15574        }
15575        Ok(())
15576    }
15577
15578    fn consume_if_exists(&mut self) -> bool {
15579        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15580        if !looks_like_if {
15581            return false;
15582        }
15583        if !matches!(
15584            self.tokens.get(self.pos + 1),
15585            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15586        ) {
15587            return false;
15588        }
15589        self.advance(); // IF
15590        self.advance(); // EXISTS
15591        true
15592    }
15593
15594    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15595    /// qualifiers after an index column ref. ASC / DESC are
15596    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15597    /// We accept and discard them since single-column BTree
15598    /// stores rows in natural key order today.
15599    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15600    /// ORDER BY key. Returns None when absent.
15601    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15602        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15603            return Ok(None);
15604        }
15605        self.advance();
15606        match self.advance() {
15607            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15608            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15609            other => Err(self.err(alloc::format!(
15610                "expected FIRST or LAST after NULLS, got {other:?}"
15611            ))),
15612        }
15613    }
15614
15615    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15616    /// rather than discarded.
15617    ///
15618    /// SPG's index does not scan in a direction — column ordering is
15619    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15620    /// reproduction of the DDL, and dropping the clause meant
15621    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15622    /// dump lost it, and a schema diff saw drift on every run.
15623    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15624        let mut order = crate::ast::IndexColumnOrder::default();
15625        loop {
15626            match self.peek() {
15627                Token::Asc => {
15628                    self.advance();
15629                }
15630                Token::Desc => {
15631                    order.descending = true;
15632                    self.advance();
15633                }
15634                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15635                    let look = self.tokens.get(self.pos + 1);
15636                    if matches!(
15637                        look,
15638                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15639                            || k.eq_ignore_ascii_case("last")
15640                    ) {
15641                        self.advance();
15642                        order.nulls_first = Some(matches!(
15643                            self.advance(),
15644                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15645                        ));
15646                    } else {
15647                        break;
15648                    }
15649                }
15650                _ => break,
15651            }
15652        }
15653        order
15654    }
15655
15656    fn parse_create_index_stmt_after_create(
15657        &mut self,
15658        is_unique: bool,
15659    ) -> Result<Statement, ParseError> {
15660        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15661        debug_assert!(matches!(self.peek(), Token::Index));
15662        self.advance();
15663        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15664        // SPG's CREATE INDEX is synchronous end-to-end today (real
15665        // CONCURRENTLY variant with restartable scans queues with
15666        // v7.39 indexes epic), so the modifier has no runtime effect
15667        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15668        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15669        // VIEW CONCURRENTLY.
15670        let mut concurrently = false;
15671        if matches!(
15672            self.peek(),
15673            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15674        ) {
15675            self.advance();
15676            concurrently = true;
15677        }
15678        let if_not_exists = self.consume_if_not_exists();
15679        // v7.39 (read01 round 93) — the index name is optional (PG since
15680        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15681        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15682        // was given; leave it empty and the engine derives a PG-style
15683        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15684        let name = if matches!(self.peek(), Token::On) {
15685            String::new()
15686        } else {
15687            self.expect_ident_like()?
15688        };
15689        if !matches!(self.peek(), Token::On) {
15690            return Err(self.err(format!(
15691                "expected ON after CREATE INDEX <name>, got {:?}",
15692                self.peek()
15693            )));
15694        }
15695        self.advance();
15696        let table = self.expect_ident_like()?;
15697        // Optional `USING <method>` — only recognised method in v2.0 is
15698        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15699        // ident `using` (we don't promote it to a reserved keyword
15700        // because it isn't reserved anywhere else in our SQL surface).
15701        let mut method_name: Option<String> = None;
15702        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15703            self.advance();
15704            let m = self.expect_ident_like()?;
15705            method_name = Some(m.to_ascii_lowercase());
15706            match m.to_ascii_lowercase().as_str() {
15707                "hnsw" => IndexMethod::Hnsw,
15708                "btree" => IndexMethod::BTree,
15709                "brin" => IndexMethod::Brin,
15710                // v7.12.3 — real GIN inverted index over `tsvector`.
15711                // v7.9.26b's `USING gin` → BTree silent fallback is
15712                // gone; the engine validates that the indexed column
15713                // is `tsvector` at CREATE INDEX time.
15714                "gin" => IndexMethod::Gin,
15715                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15716                // `USING spgist` / `USING hash` for their built-in
15717                // AMs that SPG doesn't have a matching
15718                // implementation for; degrade to BTree on the
15719                // leading column so the schema loads + the index
15720                // catalogue stays consistent. Operator pays the
15721                // planner cost only for the queries that would have
15722                // used the specialised AM.
15723                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15724                // v7.11.3 — pgvector ships both `ivfflat` and
15725                // `hnsw`. Customers shouldn't have to choose
15726                // their on-disk index method based on what SPG
15727                // implements; accept `ivfflat` as a synonym for
15728                // `hnsw` so PG schemas using either method drop
15729                // in. The vector distance op (`<->` / `<#>` /
15730                // `<=>`) at query time still picks the metric.
15731                "ivfflat" => IndexMethod::Hnsw,
15732                other => {
15733                    return Err(self.err(alloc::format!(
15734                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15735                    )));
15736                }
15737            }
15738        } else {
15739            IndexMethod::BTree
15740        };
15741        if !matches!(self.peek(), Token::LParen) {
15742            return Err(self.err(format!(
15743                "expected '(' before indexed column, got {:?}",
15744                self.peek()
15745            )));
15746        }
15747        self.advance();
15748        // v6.8.2 — accept either a bare column ident (legacy) or
15749        // an expression `fn(col, …)` for expression indexes.
15750        // Distinguish by peeking the token *after* the current
15751        // ident: `ident )` is the legacy column-only path;
15752        // anything else triggers the Pratt expression parser.
15753        // (`advance()` uses `mem::replace` to nil out the current
15754        // slot, so we can't save+rewind cleanly — peek-ahead via
15755        // direct index avoids the mutation.)
15756        let mut opclass: Option<String> = None;
15757        let mut key_collation: Option<String> = None;
15758        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15759            // Single column with `)` immediately after — fast path.
15760            // v7.9.29 — also: bare column followed by `,` (the
15761            // multi-column form `(a, b, c)`). Without this branch
15762            // the leading ident gets pulled into `parse_expr`
15763            // which then sets `expression = Some(Column(a))` and
15764            // breaks Display round-trip on the multi-column shape.
15765            Token::Ident(s) | Token::QuotedIdent(s)
15766                if matches!(
15767                    self.tokens.get(self.pos + 1),
15768                    Some(Token::RParen | Token::Comma)
15769                ) =>
15770            {
15771                self.advance();
15772                (s, None)
15773            }
15774            // v7.9.22 — single column followed by a pgvector
15775            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15776            // v7.15.0 — capture the opclass instead of discarding
15777            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15778            // → real trigram-shingle GIN over a TEXT column).
15779            // Vector/HNSW opclasses still take their distance
15780            // metric from the query operator (`<->` / `<#>` /
15781            // `<=>`), so for those callers the opclass stays
15782            // informational.
15783            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15784            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15785            // the schema and dispatch on the bare opclass, the same
15786            // treatment table/type names get.
15787            Token::Ident(s) | Token::QuotedIdent(s)
15788                if matches!(
15789                    self.tokens.get(self.pos + 1),
15790                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15791                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15792                    && matches!(
15793                        self.tokens.get(self.pos + 3),
15794                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15795                            if is_vector_opclass_name(op)
15796                    ) =>
15797            {
15798                self.advance(); // column name
15799                self.advance(); // schema qualifier
15800                self.advance(); // dot
15801                let op_tok = self.advance();
15802                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15803                    opclass = Some(op.to_ascii_lowercase());
15804                }
15805                (s, None)
15806            }
15807            // r1038 — an operator class is recognised by its POSITION, not
15808            // by a list of names. It used to be `is_vector_opclass_name`,
15809            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15810            // sentori's migration wrote — was a syntax error while
15811            // `USING gin (doc)` parsed. Anything sitting between a column
15812            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15813            // two bare identifiers in a row are not valid there otherwise.
15814            Token::Ident(s) | Token::QuotedIdent(s)
15815                if matches!(
15816                    self.tokens.get(self.pos + 1),
15817                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15818                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
15819                            self.tokens.get(self.pos + 2)
15820                        )
15821                ) =>
15822            {
15823                self.advance(); // column name
15824                // Capture the opclass token, lower-cased for
15825                // case-insensitive engine dispatch.
15826                let op_tok = self.advance();
15827                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15828                    opclass = Some(op.to_ascii_lowercase());
15829                }
15830                (s, None)
15831            }
15832            Token::Ident(_) | Token::QuotedIdent(_) => {
15833                // v7.39 (round 538) — an explicit COLLATE on the key,
15834                // read by LOOKAHEAD because `parse_expr` absorbs the
15835                // clause as a no-op (SPG orders text by bytes, which is
15836                // the C collation, so it changes nothing to honour). PG
15837                // still PRINTS it: an explicitly written `"C"` and the
15838                // collation a column inherits are different collation
15839                // OBJECTS even where they sort identically, which is why
15840                // `(a COLLATE "C")` shows on a C-collation database too.
15841                if matches!(
15842                    self.tokens.get(self.pos + 1),
15843                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15844                ) {
15845                    key_collation = match self.tokens.get(self.pos + 2) {
15846                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15847                            Some(n.clone())
15848                        }
15849                        _ => None,
15850                    };
15851                }
15852                let key_expr = self.parse_expr(0)?;
15853                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15854                    self.err("expression index key must reference at least one column".into())
15855                })?;
15856                (primary, Some(key_expr))
15857            }
15858            // v7.37.43-T4 — parenthesised expression index key
15859            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15860            // PG's CREATE INDEX requires the expression to be in
15861            // its own parens to disambiguate function calls from
15862            // column lists, so this `LParen` is the inner open-paren
15863            // of an expression key. parse_expr handles the recursive
15864            // descent and consumes the matching `RParen`.
15865            Token::LParen => {
15866                let key_expr = self.parse_expr(0)?;
15867                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15868                    self.err("expression index key must reference at least one column".into())
15869                })?;
15870                (primary, Some(key_expr))
15871            }
15872            other => {
15873                return Err(self.err(format!(
15874                    "expected column ident or expression, got {other:?}"
15875                )));
15876            }
15877        };
15878        // v7.9.14 — accept extra comma-separated columns inside
15879        // the index key parens (`CREATE INDEX … (a, b, c)`).
15880        // mailrs F2. Each extra column may carry an optional
15881        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15882        // — parsed and discarded; SPG doesn't honour direction
15883        // on a BTree index today (column ordering is intrinsic
15884        // to the storage). v7.10 will widen to genuine composite
15885        // index keys.
15886        let mut extra_columns: Vec<String> = Vec::new();
15887        // The leading column may also have ASC/DESC after it — and that
15888        // one is the column SPG indexes, so its clause is kept.
15889        let key_order = self.consume_optional_index_column_qualifiers();
15890        while matches!(self.peek(), Token::Comma) {
15891            self.advance();
15892            let extra = self.expect_ident_like()?;
15893            let _ = self.consume_optional_index_column_qualifiers();
15894            extra_columns.push(extra);
15895        }
15896        if !matches!(self.peek(), Token::RParen) {
15897            return Err(self.err(format!(
15898                "expected ')' after indexed column / expression, got {:?}",
15899                self.peek()
15900            )));
15901        }
15902        self.advance();
15903        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15904        // index-only-scan annotation. Bare ident (not a reserved
15905        // keyword) so we test by case-insensitive string match.
15906        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15907        {
15908            self.advance();
15909            if !matches!(self.peek(), Token::LParen) {
15910                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15911            }
15912            self.advance();
15913            let mut cols = Vec::new();
15914            loop {
15915                cols.push(self.expect_ident_like()?);
15916                match self.peek() {
15917                    Token::Comma => {
15918                        self.advance();
15919                    }
15920                    Token::RParen => {
15921                        self.advance();
15922                        break;
15923                    }
15924                    other => {
15925                        return Err(self.err(format!(
15926                            "expected ',' or ')' in INCLUDE list, got {other:?}"
15927                        )));
15928                    }
15929                }
15930            }
15931            cols
15932        } else {
15933            Vec::new()
15934        };
15935        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15936        // storage parameters. pgvector emits `WITH (lists = N)` for
15937        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15938        // SPG's HNSW picks its own parameters today (tunable via
15939        // env vars), so the WITH clause is informational and dropped.
15940        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15941            self.advance();
15942            if !matches!(self.peek(), Token::LParen) {
15943                return Err(self.err(format!(
15944                    "expected '(' after WITH in CREATE INDEX, got {:?}",
15945                    self.peek()
15946                )));
15947            }
15948            self.advance();
15949            loop {
15950                if matches!(self.peek(), Token::RParen) {
15951                    self.advance();
15952                    break;
15953                }
15954                // Drain `key = value` or bare `key` tokens.
15955                let _ = self.advance(); // key
15956                if matches!(self.peek(), Token::Eq) {
15957                    self.advance();
15958                    let _ = self.advance(); // value (int / string / ident)
15959                }
15960                match self.peek() {
15961                    Token::Comma => {
15962                        self.advance();
15963                    }
15964                    Token::RParen => {
15965                        self.advance();
15966                        break;
15967                    }
15968                    other => {
15969                        return Err(self.err(format!(
15970                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
15971                        )));
15972                    }
15973                }
15974            }
15975        }
15976        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15977        // which sits between the key list and the WHERE clause.
15978        let mut nulls_not_distinct = false;
15979        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15980            let n1 = self.tokens.get(self.pos + 1);
15981            let n2 = self.tokens.get(self.pos + 2);
15982            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15983                self.advance(); // NULLS
15984                self.advance(); // NOT
15985                self.advance(); // DISTINCT
15986                nulls_not_distinct = true;
15987            } else if matches!(n1, Some(Token::Distinct)) {
15988                self.advance(); // NULLS
15989                self.advance(); // DISTINCT
15990            }
15991        }
15992        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15993        let partial_predicate = if matches!(self.peek(), Token::Where) {
15994            self.advance();
15995            Some(self.parse_expr(0)?)
15996        } else {
15997            None
15998        };
15999        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16000        // sense: uniqueness over an ANN structure has no clean
16001        // semantics. Reject early. (BRIN UNIQUE is similarly
16002        // meaningless — block both.)
16003        if is_unique && !matches!(method, IndexMethod::BTree) {
16004            return Err(self.err(alloc::format!(
16005                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16006                method
16007            )));
16008        }
16009        Ok(Statement::CreateIndex(CreateIndexStatement {
16010            concurrently,
16011            name,
16012            key_order,
16013            key_collation,
16014            table,
16015            column,
16016            nulls_not_distinct,
16017            method,
16018            if_not_exists,
16019            included_columns,
16020            partial_predicate,
16021            extra_columns: extra_columns.clone(),
16022            expression,
16023            is_unique,
16024            opclass,
16025            method_name,
16026        }))
16027    }
16028
16029    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16030    /// column-level `REFERENCES ...` clause. The trailing FK is
16031    /// normalised into table-level shape (single-element columns +
16032    /// parent_columns) so the engine sees one uniform constraint list.
16033    fn parse_column_def_with_fk(
16034        &mut self,
16035    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16036        let col = self.parse_column_def()?;
16037        // v7.39 (round 308, V29) — an explicitly named inline FK:
16038        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16039        // loop leaves this spelling intact precisely so the name can be
16040        // kept here; PG reports it in violation messages and matches it
16041        // in `SET CONSTRAINTS`.
16042        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16043        {
16044            self.advance();
16045            Some(self.expect_ident_like()?)
16046        } else {
16047            None
16048        };
16049        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16050        let inline_references = matches!(
16051            self.peek(),
16052            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16053        );
16054        if !inline_references {
16055            return Ok((col, None));
16056        }
16057        let (
16058            parent_table,
16059            parent_columns,
16060            on_delete,
16061            on_update,
16062            match_type,
16063            deferrable,
16064            initially_deferred,
16065        ) = self.parse_references_tail(1)?;
16066        let fk = ForeignKeyConstraint {
16067            name: declared_name,
16068            columns: vec![col.name.clone()],
16069            parent_table,
16070            parent_columns,
16071            on_delete,
16072            on_update,
16073            match_type,
16074            deferrable,
16075            initially_deferred,
16076        };
16077        Ok((col, Some(fk)))
16078    }
16079
16080    /// v7.13.0 — parse a column type (consuming the type ident and
16081    /// any trailing parameters / `[]`), without surrounding column
16082    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16083    /// Returns the resolved `ColumnTypeName` plus implied
16084    /// `(auto_increment, not_null)` flags from PG SERIAL family
16085    /// shorthands — callers that don't expect those (ALTER COLUMN
16086    /// TYPE) can discard them.
16087    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16088        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16089        Ok(ty)
16090    }
16091
16092    #[allow(clippy::type_complexity)]
16093    fn parse_type_with_implied_flags(
16094        &mut self,
16095    ) -> Result<
16096        (
16097            ColumnTypeName,
16098            bool,
16099            bool,
16100            Option<String>,
16101            Collation,
16102            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16103            bool,
16104            // v7.39 (round 676) — the collation NAME as written, which the
16105            // `Collation` enum above cannot carry.
16106            Option<String>,
16107            bool,
16108            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16109            // list captured at type-parse time. None for all
16110            // non-ENUM types.
16111            Option<Vec<String>>,
16112            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16113            // list. Distinct from ENUM (subset semantics).
16114            Option<Vec<String>>,
16115            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16116            // width, lost when the type collapses to SmallInt / Int.
16117            Option<MysqlIntWidth>,
16118            // v7.39 (round 424) — declared fractional-seconds precision of a
16119            // MySQL temporal column (bare spelling = 0). None under PG.
16120            Option<u8>,
16121        ),
16122        ParseError,
16123    > {
16124        let mut ty_ident = match self.advance() {
16125            Token::Ident(s) => s,
16126            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16127            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16128            // '<span>'` literal grammar. As a column type it lands
16129            // here directly; downstream resolution still uses the
16130            // canonical lowercase string.
16131            Token::Interval => "interval".to_string(),
16132            other => {
16133                return Err(ParseError {
16134                    message: format!("expected column type, got {other:?}"),
16135                    token_pos: self.consumed_pos(),
16136                });
16137            }
16138        };
16139        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16140        // pg_dump qualifies extension types (`public.vector(1024)`).
16141        // SPG is single-namespace; drop the schema and resolve the
16142        // bare type — same treatment table names already get.
16143        while matches!(self.peek(), Token::Dot) {
16144            self.advance();
16145            ty_ident = self.expect_ident_like()?;
16146        }
16147        let mut implied_auto_increment = false;
16148        let mut implied_not_null = false;
16149        let mut user_type_ref: Option<String> = None;
16150        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16151        // value list, captured here and bubbled up through the
16152        // ColumnDef so the engine can attach it to the column
16153        // schema (and validate INSERT cells against it).
16154        let mut inline_enum_variants: Option<Vec<String>> = None;
16155        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16156        let mut inline_set_variants: Option<Vec<String>> = None;
16157        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16158        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16159        // collapses to SmallInt / Int. Only under the MySQL dialect.
16160        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16161        // v7.39 (round 424) — the declared fractional-seconds precision of a
16162        // MySQL temporal column. Set by the temporal arms below; stays None
16163        // for PG (whose temporal columns keep full microseconds).
16164        let mut mysql_fsp: Option<u8> = None;
16165        let mut ty = match ty_ident.as_str() {
16166            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16167            "smallserial" | "serial2" => {
16168                implied_auto_increment = true;
16169                implied_not_null = true;
16170                ColumnTypeName::SmallInt
16171            }
16172            "serial" | "serial4" => {
16173                implied_auto_increment = true;
16174                implied_not_null = true;
16175                ColumnTypeName::Int
16176            }
16177            "bigserial" | "serial8" => {
16178                implied_auto_increment = true;
16179                implied_not_null = true;
16180                ColumnTypeName::BigInt
16181            }
16182            // MySQL flavours we accept by aliasing to the closest SPG
16183            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16184            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16185            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16186            // without semantic effect.
16187            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16188            // PG's internal type names; pg_dump and hand-written PG schemas
16189            // use them interchangeably with smallint / int / bigint (the cast
16190            // path already accepted them, only the column grammar didn't).
16191            "smallint" | "int2" => {
16192                // v7.14.0 — MySQL display-width on integers
16193                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16194                // parenthesised number is purely cosmetic — it
16195                // doesn't change storage. Accept + discard.
16196                self.consume_optional_paren_size();
16197                ColumnTypeName::SmallInt
16198            }
16199            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16200            // canonical encoding for BOOLEAN. Every MySQL driver
16201            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16202            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16203            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16204            // gave the customer i16-shaped values where the app
16205            // expected bool — a Tier-A silent type drift on
16206            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16207            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16208            // stay SmallInt (the legacy width-agnostic path).
16209            "tinyint" => {
16210                let width = self.peek_optional_paren_size_value();
16211                self.consume_optional_paren_size();
16212                if width == Some(1) {
16213                    ColumnTypeName::Bool
16214                } else {
16215                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16216                    // lost width so the write path can enforce -128..127.
16217                    if self.mysql_dialect {
16218                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16219                    }
16220                    ColumnTypeName::SmallInt
16221                }
16222            }
16223            "mediumint" => {
16224                self.consume_optional_paren_size();
16225                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16226                if self.mysql_dialect {
16227                    mysql_int_width = Some(MysqlIntWidth::Medium);
16228                }
16229                ColumnTypeName::Int
16230            }
16231            "int" | "integer" | "int4" => {
16232                self.consume_optional_paren_size();
16233                ColumnTypeName::Int
16234            }
16235            "bigint" | "int8" => {
16236                self.consume_optional_paren_size();
16237                ColumnTypeName::BigInt
16238            }
16239            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16240            // (mailrs round-5 G6). Consume the optional `PRECISION`
16241            // tail when the type keyword was `double` / `DOUBLE`.
16242            //
16243            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16244            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16245            // p in 1..=24 is real, 25..=53 is double precision, and
16246            // anything else is an error.
16247            "float" | "double" | "real" => {
16248                if ty_ident.eq_ignore_ascii_case("double")
16249                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16250                {
16251                    self.advance();
16252                }
16253                if ty_ident.eq_ignore_ascii_case("real") {
16254                    // v7.39 (round 274) — the two dialects genuinely
16255                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16256                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16257                    // 32-bit globally and thereby narrowed the stored
16258                    // precision of every MySQL REAL column.
16259                    if self.mysql_dialect {
16260                        ColumnTypeName::Float
16261                    } else {
16262                        ColumnTypeName::Real
16263                    }
16264                } else if ty_ident.eq_ignore_ascii_case("float")
16265                    && self.mysql_dialect
16266                    && matches!(self.peek(), Token::LParen)
16267                    && self.peek_paren_has_comma()
16268                {
16269                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16270                    // display form (`FLOAT(10,2)`), which PG has no
16271                    // equivalent of. It was `syntax error at or near ","`,
16272                    // so the whole CREATE failed. The digits are a display
16273                    // hint only; SPG stores the full double.
16274                    self.consume_optional_paren_size();
16275                    ColumnTypeName::Float
16276                } else if ty_ident.eq_ignore_ascii_case("float")
16277                    && matches!(self.peek(), Token::LParen)
16278                {
16279                    // PG words the two bounds differently, and
16280                    // parse_paren_size already rejects a zero.
16281                    let p = self.parse_paren_size("FLOAT")?;
16282                    if p > 53 {
16283                        return Err(self.err(String::from(
16284                            "precision for type float must be less than 54 bits",
16285                        )));
16286                    }
16287                    if p <= 24 {
16288                        ColumnTypeName::Real
16289                    } else {
16290                        ColumnTypeName::Float
16291                    }
16292                } else {
16293                    ColumnTypeName::Float
16294                }
16295            }
16296            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16297            "float4" => ColumnTypeName::Real,
16298            "float8" => ColumnTypeName::Float,
16299            "text" => ColumnTypeName::Text,
16300            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16301            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16302            // real MySQL schema and NONE of them existed: the CREATE
16303            // failed outright with `type "blob" does not exist`, so the
16304            // table was never made. The sizes differ only in MySQL's
16305            // maximum length, which SPG does not cap, so they collapse
16306            // onto TEXT and BYTEA the way the unsized spellings do.
16307            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16308            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16309            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16310            // enforce, consumed so the declaration parses.
16311            "varbinary" | "binary" => {
16312                self.consume_optional_paren_size();
16313                ColumnTypeName::Bytes
16314            }
16315            "name" => ColumnTypeName::Name,
16316            "xid" => ColumnTypeName::Xid,
16317            "oid" => ColumnTypeName::Oid,
16318            "xid8" => ColumnTypeName::Xid8,
16319            "bool" | "boolean" => ColumnTypeName::Bool,
16320            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16321            // an unbounded `character varying`, which the arm below has always
16322            // read as text. Only the short spelling demanded a length, so
16323            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16324            // there is — failed on `VARCHAR type requires (N)` while the long
16325            // spelling of the same thing was accepted. The same asymmetry
16326            // round 613 closed on the CAST side, here on the DDL side.
16327            "varchar" => {
16328                if matches!(self.peek(), Token::LParen) {
16329                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16330                } else {
16331                    ColumnTypeName::Text
16332                }
16333            }
16334            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16335            // `character` below (SQL standard).
16336            "char" => {
16337                if matches!(self.peek(), Token::LParen) {
16338                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16339                } else {
16340                    ColumnTypeName::Char(1)
16341                }
16342            }
16343            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16344            // `character(n)` = char, bare `character` = char(1). Unbounded
16345            // `character varying` maps to text.
16346            "character" => {
16347                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16348                    self.advance();
16349                    if matches!(self.peek(), Token::LParen) {
16350                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16351                    } else {
16352                        ColumnTypeName::Text
16353                    }
16354                } else if matches!(self.peek(), Token::LParen) {
16355                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16356                } else {
16357                    ColumnTypeName::Char(1)
16358                }
16359            }
16360            "vector" => {
16361                let dim = self.parse_paren_size("VECTOR")?;
16362                let encoding = self.parse_optional_vector_encoding()?;
16363                ColumnTypeName::Vector { dim, encoding }
16364            }
16365            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16366            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16367            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16368            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16369            // DECIMAL(10,2))` — how nearly every money column is written,
16370            // in either dialect — was a syntax error and the table was
16371            // never created. `FIXED` is MySQL's alias alone, so it is
16372            // taken only in that dialect.
16373            "numeric" | "decimal" | "dec" => {
16374                let (precision, scale) = self.parse_optional_numeric_params()?;
16375                ColumnTypeName::Numeric(precision, scale)
16376            }
16377            "fixed" if self.mysql_dialect => {
16378                let (precision, scale) = self.parse_optional_numeric_params()?;
16379                ColumnTypeName::Numeric(precision, scale)
16380            }
16381            "date" => ColumnTypeName::Date,
16382            // MySQL's `DATETIME` is the same domain as standard
16383            // `TIMESTAMP` — accept both spellings.
16384            "timestamp" | "datetime" => {
16385                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16386                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16387                // TIME ZONE` clause, so consume it first.
16388                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16389                // (it truncates on write and pads on render), so capture it;
16390                // a bare spelling means precision 0 there. PG stores µs always
16391                // and keeps `None`.
16392                let n = self.take_optional_paren_size();
16393                if self.mysql_dialect {
16394                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16395                }
16396                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16397                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16398                // the full form. SPG canonicalises:
16399                //   - WITH TIME ZONE    → Timestamptz
16400                //   - WITHOUT TIME ZONE → Timestamp
16401                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16402                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16403                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16404                {
16405                    self.advance(); // WITH
16406                    self.advance(); // TIME
16407                    self.advance(); // ZONE
16408                    ColumnTypeName::Timestamptz
16409                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16410                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16411                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16412                {
16413                    self.advance(); // WITHOUT
16414                    self.advance(); // TIME
16415                    self.advance(); // ZONE
16416                    ColumnTypeName::Timestamp
16417                } else {
16418                    // A second `(precision)` cannot legally follow, but the
16419                    // old grammar tolerated it; keep that tolerance.
16420                    self.consume_optional_paren_size();
16421                    ColumnTypeName::Timestamp
16422                }
16423            }
16424            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16425            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16426            // only PG-wire OID differs.
16427            "timestamptz" => {
16428                self.consume_optional_paren_size();
16429                ColumnTypeName::Timestamptz
16430            }
16431            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16432            // validation. We accept the JSONB spelling too because
16433            // most PG clients default to it; SPG doesn't distinguish
16434            // the two (no path-operator perf advantage to model).
16435            "json" => ColumnTypeName::Json,
16436            "jsonb" => ColumnTypeName::Jsonb,
16437            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16438            // surface here. Same storage shape; mapping happens at
16439            // the engine side via the ColumnTypeName → DataType
16440            // resolver. Literal forms are handled at coerce_value
16441            // time so the lexer stays untouched.
16442            "bytea" | "bytes" => ColumnTypeName::Bytes,
16443            // v7.17.0 Phase 7 — PG network address types
16444            // v7.17.0 had a Text-backed fallback here for
16445            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16446            // each to a first-class type; the keywords are
16447            // bound below in the ζ-A block.
16448            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16449            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16450            // arrives in v7.12.1+; the type itself loads here so
16451            // mailrs's `scripts/init-schema.sql` runs unmodified.
16452            "tsvector" => ColumnTypeName::TsVector,
16453            "tsquery" => ColumnTypeName::TsQuery,
16454            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16455            // surface for Django / Rails / Hibernate's default
16456            // PK pattern.
16457            "uuid" => ColumnTypeName::Uuid,
16458            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16459            // Storage = three-field {months, days, micros}, catalog
16460            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16461            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16462            "interval" => {
16463                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16464                // SECOND` and an optional `(p)` precision. SPG stores the full
16465                // {months,days,micros}; consume + ignore the qualifier/precision.
16466                while matches!(self.peek(), Token::To)
16467                    || matches!(self.peek(), Token::Ident(s) if matches!(
16468                        s.to_ascii_lowercase().as_str(),
16469                        "year" | "month" | "day" | "hour" | "minute" | "second"
16470                    ))
16471                {
16472                    self.advance();
16473                }
16474                self.consume_optional_paren_size();
16475                ColumnTypeName::Interval
16476            }
16477            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16478            // i64 microseconds since 00:00:00. Wire OID 1083.
16479            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16480            "time" => {
16481                // v7.39 (round 424) — MySQL TIME carries a semantic
16482                // fractional-seconds precision, bare meaning 0.
16483                let n = self.take_optional_paren_size();
16484                if self.mysql_dialect {
16485                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16486                }
16487                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16488                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16489                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16490                {
16491                    self.advance();
16492                    self.advance();
16493                    self.advance();
16494                    ColumnTypeName::TimeTz
16495                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16496                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16497                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16498                {
16499                    self.advance();
16500                    self.advance();
16501                    self.advance();
16502                    ColumnTypeName::Time
16503                } else {
16504                    ColumnTypeName::Time
16505                }
16506            }
16507            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16508            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16509            "year" => ColumnTypeName::Year,
16510            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16511            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16512            "timetz" => ColumnTypeName::TimeTz,
16513            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16514            // Wire OID 790.
16515            "money" => ColumnTypeName::Money,
16516            // v7.17.0 Phase 3.P0-38 — PG range types.
16517            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16518            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16519            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16520            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16521            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16522            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16523            // v7.37.5 δ — PG 14+ multirange keywords.
16524            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16525            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16526            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16527            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16528            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16529            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16530            // v7.37.5 ε — PG geometry scalar keywords.
16531            "point" => ColumnTypeName::Point,
16532            "lseg" => ColumnTypeName::Lseg,
16533            "path" => ColumnTypeName::Path,
16534            "box" => ColumnTypeName::PgBox,
16535            "polygon" => ColumnTypeName::Polygon,
16536            "line" => ColumnTypeName::Line,
16537            "circle" => ColumnTypeName::Circle,
16538            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16539            "inet" => ColumnTypeName::Inet,
16540            "cidr" => ColumnTypeName::Cidr,
16541            "macaddr" => ColumnTypeName::Macaddr,
16542            "macaddr8" => ColumnTypeName::Macaddr8,
16543            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16544            // width in the value, so the optional `(N)` typmod is accepted and
16545            // ignored (the column stores whatever width it's given).
16546            "bit" => {
16547                let varying = matches!(
16548                    self.peek(),
16549                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16550                );
16551                if varying {
16552                    self.advance();
16553                }
16554                // v7.39 (round 281) — the length used to be parsed and
16555                // dropped, so `bit(3)` accepted a five-bit string.
16556                let n = if matches!(self.peek(), Token::LParen) {
16557                    self.parse_paren_size("BIT")?
16558                } else {
16559                    0
16560                };
16561                if varying {
16562                    ColumnTypeName::BitVarying(n)
16563                } else {
16564                    ColumnTypeName::Bit(n)
16565                }
16566            }
16567            "varbit" => {
16568                let n = if matches!(self.peek(), Token::LParen) {
16569                    self.parse_paren_size("VARBIT")?
16570                } else {
16571                    0
16572                };
16573                ColumnTypeName::BitVarying(n)
16574            }
16575            "xml" => ColumnTypeName::Xml,
16576            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16577            "hstore" => ColumnTypeName::Hstore,
16578            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16579            // `ENUM('a','b','c')`. Storage is TEXT; the value
16580            // list lands on `inline_enum_variants` for the
16581            // engine to validate INSERT cells against. Empty
16582            // value list is a parse error (matches MySQL).
16583            "enum" => {
16584                // Expect the opening `(`.
16585                if !matches!(self.peek(), Token::LParen) {
16586                    return Err(self.err(alloc::format!(
16587                        "expected '(' after ENUM, got {:?}",
16588                        self.peek()
16589                    )));
16590                }
16591                self.advance();
16592                let mut variants: Vec<String> = Vec::new();
16593                loop {
16594                    match self.advance() {
16595                        Token::String(s) => variants.push(s),
16596                        other => {
16597                            return Err(self.err(alloc::format!(
16598                                "ENUM(...) expects string literal variants, got {other:?}"
16599                            )));
16600                        }
16601                    }
16602                    match self.peek() {
16603                        Token::Comma => {
16604                            self.advance();
16605                            continue;
16606                        }
16607                        Token::RParen => {
16608                            self.advance();
16609                            break;
16610                        }
16611                        other => {
16612                            return Err(self.err(alloc::format!(
16613                                "expected ',' or ')' in ENUM(...), got {other:?}"
16614                            )));
16615                        }
16616                    }
16617                }
16618                if variants.is_empty() {
16619                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16620                }
16621                inline_enum_variants = Some(variants);
16622                // Storage is plain TEXT; the variant list lives on
16623                // the ColumnSchema side.
16624                ColumnTypeName::Text
16625            }
16626            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16627            // `SET('a','b','c')`. Same parse shape as ENUM;
16628            // semantics differ (subset rather than pick-one).
16629            "set" => {
16630                if !matches!(self.peek(), Token::LParen) {
16631                    return Err(self.err(alloc::format!(
16632                        "expected '(' after SET, got {:?}",
16633                        self.peek()
16634                    )));
16635                }
16636                self.advance();
16637                let mut variants: Vec<String> = Vec::new();
16638                loop {
16639                    match self.advance() {
16640                        Token::String(s) => variants.push(s),
16641                        other => {
16642                            return Err(self.err(alloc::format!(
16643                                "SET(...) expects string literal variants, got {other:?}"
16644                            )));
16645                        }
16646                    }
16647                    match self.peek() {
16648                        Token::Comma => {
16649                            self.advance();
16650                            continue;
16651                        }
16652                        Token::RParen => {
16653                            self.advance();
16654                            break;
16655                        }
16656                        other => {
16657                            return Err(self.err(alloc::format!(
16658                                "expected ',' or ')' in SET(...), got {other:?}"
16659                            )));
16660                        }
16661                    }
16662                }
16663                if variants.is_empty() {
16664                    return Err(self.err("SET(...) must declare at least one variant".into()));
16665                }
16666                inline_set_variants = Some(variants);
16667                ColumnTypeName::Text
16668            }
16669            _other => {
16670                // v7.17.0 Phase 1.4 — unknown ident → defer
16671                // resolution to the engine. Stored as Text in
16672                // ColumnTypeName + the original name carried as
16673                // `user_type_ref` so CREATE TABLE can look up
16674                // user-defined enum / domain types.
16675                user_type_ref = Some(ty_ident.clone());
16676                ColumnTypeName::Text
16677            }
16678        };
16679        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16680        // right after the type keyword. Pre-4.4 SPG consumed +
16681        // discarded the keyword, leaving a customer column
16682        // declared `id INT UNSIGNED NOT NULL` silently accepting
16683        // negative values — a Tier-A correctness drift where
16684        // application invariants (auto-increment-IDs never
16685        // negative) silently broke on cutover. Now: capture as
16686        // a column flag, persist on the schema, enforce at
16687        // INSERT / UPDATE time.
16688        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16689        {
16690            self.advance();
16691            true
16692        } else {
16693            false
16694        };
16695        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16696        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16697        // stores text as UTF-8 always so CHARACTER SET is still a
16698        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16699        // name: it gets classified into a `Collation` variant the
16700        // engine consults at WHERE-eval time. PG `default` /
16701        // `pg_catalog.default` / `C` / `POSIX` collations all
16702        // resolve to `Binary` (the prior behaviour); `_ci` /
16703        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16704        // The schema-qualifier form (`pg_catalog.default`) lexes
16705        // as `Ident '.' Ident` — peek for the `.` and consume both
16706        // halves so it's treated as one collation name. PG's
16707        // `IDENT.IDENT` collation form (which can appear here) is
16708        // resolved by Collation::from_collation_name on the bare
16709        // identifier after the dot.
16710        let mut collation = Collation::Binary;
16711        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16712        // clause was written. The engine needs this to tell an explicit
16713        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16714        // clause at all: both resolve to `Collation::Binary`, but under the
16715        // MySQL dialect the latter takes the folding default collation.
16716        let mut collation_explicit = false;
16717        let mut collation_name: Option<alloc::string::String> = None;
16718        loop {
16719            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16720                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16721            {
16722                self.advance(); // CHARACTER
16723                self.advance(); // SET
16724                if matches!(
16725                    self.peek(),
16726                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16727                ) {
16728                    self.advance();
16729                }
16730                continue;
16731            }
16732            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16733                self.advance(); // COLLATE
16734                // Accept Ident / QuotedIdent / String AND the
16735                // keyword-tokenised `Default` (PG `pg_catalog.default`
16736                // and bare `DEFAULT` collation names — `default` is a
16737                // reserved word so the lexer hands back Token::Default
16738                // not Token::Ident).
16739                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16740                    match this.peek().clone() {
16741                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16742                            this.advance();
16743                            Some(s)
16744                        }
16745                        Token::Default => {
16746                            this.advance();
16747                            Some(alloc::string::String::from("default"))
16748                        }
16749                        _ => None,
16750                    }
16751                };
16752                let raw = if let Some(head) = read_collation_atom(self) {
16753                    // Schema-qualified PG form: `pg_catalog.default`.
16754                    if matches!(self.peek(), Token::Dot) {
16755                        self.advance();
16756                        let tail = read_collation_atom(self).unwrap_or_default();
16757                        alloc::format!("{head}.{tail}")
16758                    } else {
16759                        head
16760                    }
16761                } else {
16762                    alloc::string::String::new()
16763                };
16764                if !raw.is_empty() {
16765                    collation_explicit = true;
16766                    // v7.39 (round 676) — keep the name too. The enum below
16767                    // folds C / POSIX / en_US / default into one value, and
16768                    // `pg_attribute.attcollation` has to tell them apart.
16769                    // The schema qualifier goes: PG's `pg_catalog.default`
16770                    // and a bare `default` name the same collation.
16771                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16772                    // encoding suffix. Round 676 used `rsplit('.')` for
16773                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16774                    // PG writes `pg_catalog.default` (qualifier) and
16775                    // `en_US.utf8` (locale + encoding) with the same
16776                    // separator. Only `pg_catalog.` is a qualifier, and it
16777                    // is the only one PG's own dumps emit.
16778                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16779                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16780                    collation_name = Some(alloc::string::String::from(bare));
16781                    let parsed = Collation::from_collation_name(&raw);
16782                    // Last COLLATE clause wins, but `Binary` from a
16783                    // bare keyword like `default` should not
16784                    // silently downgrade a stronger one set earlier
16785                    // on the same column. v7.17 only ships one
16786                    // non-Binary variant so a simple OR is enough.
16787                    if parsed != Collation::Binary {
16788                        collation = parsed;
16789                    }
16790                }
16791                continue;
16792            }
16793            break;
16794        }
16795        // v7.10.10 — postfix `[]` widens the base type to its array
16796        // type. PG accepts `TYPE[]` after any base type and so does
16797        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16798        // all through; the old "only TEXT[]" note was stale).
16799        if matches!(self.peek(), Token::LBracket) {
16800            self.advance();
16801            if !matches!(self.peek(), Token::RBracket) {
16802                return Err(self.err(alloc::format!(
16803                    "TEXT[] takes no dimension; got {:?}",
16804                    self.peek()
16805                )));
16806            }
16807            self.advance();
16808            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16809            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16810            // still error here.
16811            ty = match ty {
16812                ColumnTypeName::Text => ColumnTypeName::TextArray,
16813                ColumnTypeName::Int => ColumnTypeName::IntArray,
16814                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16815                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16816                // `[]` grammar. Wire OID 1187.
16817                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16818                // v7.37.5 γ — full PG array-of-scalar family.
16819                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16820                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16821                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16822                // NUMERIC(p, s) loses its precision params at the
16823                // array level (matches PG: `NUMERIC[]` is untyped,
16824                // per-element precision flows through values).
16825                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16826                ColumnTypeName::Date => ColumnTypeName::DateArray,
16827                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16828                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16829                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16830                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16831                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16832                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16833                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16834                // the array level (matches PG semantics where the
16835                // element precision is per-row, not column-wide).
16836                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16837                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16838                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16839                // follow-up.
16840                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16841                other => {
16842                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16843                }
16844            };
16845            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16846            // for INT/TEXT/BIGINT. Anything else is an error.
16847            if matches!(self.peek(), Token::LBracket) {
16848                self.advance();
16849                if !matches!(self.peek(), Token::RBracket) {
16850                    return Err(self.err(alloc::format!(
16851                        "TYPE[][] second dimension takes no size; got {:?}",
16852                        self.peek()
16853                    )));
16854                }
16855                self.advance();
16856                ty = match ty {
16857                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16858                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16859                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16860                    // v7.39 (read01 round 75) — bool[][].
16861                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16862                    other => {
16863                        return Err(self.err(alloc::format!(
16864                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16865                             TEXT[][] only; got {other:?}"
16866                        )));
16867                    }
16868                };
16869            }
16870        }
16871        Ok((
16872            ty,
16873            implied_auto_increment,
16874            implied_not_null,
16875            user_type_ref,
16876            collation,
16877            collation_explicit,
16878            collation_name,
16879            is_unsigned,
16880            inline_enum_variants,
16881            inline_set_variants,
16882            mysql_int_width,
16883            mysql_fsp,
16884        ))
16885    }
16886
16887    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16888        // v7.20 — PG reserves the table-constraint keywords, so a
16889        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16890        // malformed constraint clause (e.g. `UNIQUE a` missing its
16891        // parens), not a column named "unique". Since v7.17's
16892        // unknown-type leniency (`user_type_ref`) such a clause
16893        // would otherwise parse as a column with a user-defined
16894        // type — silently accepting invalid DDL. Quoted
16895        // identifiers ("unique" / `unique`) remain valid names.
16896        if let Token::Ident(s) = self.peek()
16897            && [
16898                "unique",
16899                "primary",
16900                "foreign",
16901                "constraint",
16902                "check",
16903                "references",
16904                "exclude",
16905            ]
16906            .iter()
16907            .any(|kw| s.eq_ignore_ascii_case(kw))
16908        {
16909            return Err(self.err(alloc::format!(
16910                "unexpected reserved keyword '{s}' at start of column definition \
16911                 (malformed table constraint?)"
16912            )));
16913        }
16914        let name = self.expect_ident_like()?;
16915        let (
16916            ty,
16917            implied_auto_increment,
16918            implied_not_null,
16919            user_type_ref,
16920            collation,
16921            collation_explicit,
16922            collation_name,
16923            is_unsigned,
16924            inline_enum_variants,
16925            inline_set_variants,
16926            mysql_int_width,
16927            mysql_fsp,
16928        ) = self.parse_type_with_implied_flags()?;
16929        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16930        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16931        // each at most once.
16932        let mut default: Option<Expr> = None;
16933        let mut nullable = !implied_not_null;
16934        let mut nullability_seen = implied_not_null;
16935        let mut auto_increment = implied_auto_increment;
16936        let mut is_primary_key = false;
16937        let mut is_unique = false;
16938        let mut unique_nulls_not_distinct = false;
16939        let mut constraint_deferrable = false;
16940        let mut constraint_initially_deferred = false;
16941        let mut check: Option<Expr> = None;
16942        let mut on_update_runtime: Option<Expr> = None;
16943        let mut generated_stored_expr: Option<Box<Expr>> = None;
16944        let mut identity_always = false;
16945        loop {
16946            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16947            // not-null constraints by name and pg_dump emits them
16948            // inline: `id bigint CONSTRAINT contacts_id_not_null1
16949            // NOT NULL`. Accept and discard the name; whatever
16950            // constraint follows is parsed by the arms below.
16951            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16952                // v7.39 (round 308, V29) — a name on an inline
16953                // REFERENCES belongs to the FOREIGN KEY, and the caller
16954                // (`parse_column_def_with_fk`) is what builds it, so
16955                // leave the whole clause for it. Dropping the name here
16956                // is what made `CONSTRAINT fk_a REFERENCES …` come back
16957                // as the synthesised `c_pid_fkey` — which then could
16958                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16959                // `advance()` takes tokens by `mem::replace`, so there
16960                // is no rewinding once consumed.
16961                if matches!(
16962                    self.tokens.get(self.pos + 2),
16963                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16964                ) {
16965                    break;
16966                }
16967                self.advance();
16968                let _name = self.expect_ident_like()?;
16969                continue;
16970            }
16971            // v7.39 (round 379) — MySQL's SHORT generated-column form
16972            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16973            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16974            // below), but hand-written schemas and app migrations use this.
16975            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16976            // SPG computes-and-stores either way, like the long form.
16977            if matches!(self.peek(), Token::As) {
16978                self.advance();
16979                if !matches!(self.peek(), Token::LParen) {
16980                    return Err(self.err(alloc::format!(
16981                        "expected '(' after AS in a generated column, got {:?}",
16982                        self.peek()
16983                    )));
16984                }
16985                self.advance();
16986                let expr = self.parse_expr(0)?;
16987                if !matches!(self.peek(), Token::RParen) {
16988                    return Err(self.err(alloc::format!(
16989                        "expected ')' after AS (<expr>), got {:?}",
16990                        self.peek()
16991                    )));
16992                }
16993                self.advance();
16994                if matches!(self.peek(), Token::Ident(s)
16995                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16996                {
16997                    self.advance();
16998                }
16999                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17000                continue;
17001            }
17002            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17003            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17004            // the modern replacement for SERIAL in hand-written
17005            // schemas). Both flavours map onto the auto-increment
17006            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17007            // ALWAYS's reject-explicit-values nuance is documented
17008            // leniency. Generated EXPRESSION columns
17009            // (`AS (expr) STORED`) are not supported: error loudly
17010            // instead of silently storing NULLs.
17011            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17012                self.advance();
17013                let mut saw_generated_always = false;
17014                match self.peek().clone() {
17015                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17016                        self.advance();
17017                        saw_generated_always = true;
17018                    }
17019                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17020                        self.advance();
17021                        if !matches!(self.peek(), Token::Default) {
17022                            return Err(self.err(alloc::format!(
17023                                "expected DEFAULT after GENERATED BY, got {:?}",
17024                                self.peek()
17025                            )));
17026                        }
17027                        self.advance();
17028                    }
17029                    other => {
17030                        return Err(self.err(alloc::format!(
17031                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17032                        )));
17033                    }
17034                }
17035                if !matches!(self.peek(), Token::As) {
17036                    return Err(self.err(alloc::format!(
17037                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17038                        self.peek()
17039                    )));
17040                }
17041                self.advance();
17042                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17043                // ( <expr> ) STORED` stored computed-column. The
17044                // expression is captured for the engine to recompute
17045                // on every INSERT / UPDATE. v7.37.7 accepts the
17046                // STORED keyword only; PG also has VIRTUAL, which
17047                // v7.37.7 carves out (sentori only uses STORED).
17048                if matches!(self.peek(), Token::LParen) {
17049                    self.advance();
17050                    let expr = self.parse_expr(0)?;
17051                    if !matches!(self.peek(), Token::RParen) {
17052                        return Err(self.err(alloc::format!(
17053                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17054                            self.peek()
17055                        )));
17056                    }
17057                    self.advance();
17058                    let stored = match self.peek() {
17059                        Token::Ident(s) | Token::QuotedIdent(s)
17060                            if s.eq_ignore_ascii_case("stored") =>
17061                        {
17062                            self.advance();
17063                            true
17064                        }
17065                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17066                        // generated columns. SPG computes them on write and
17067                        // persists like STORED; the two are observably
17068                        // identical for query results (the value, recompute
17069                        // on base-column change, and NOT NULL enforcement all
17070                        // match), so a PG 18 schema/dump using VIRTUAL loads
17071                        // and behaves correctly. The compute-on-read storage
17072                        // saving is an invisible internal difference.
17073                        Token::Ident(s) | Token::QuotedIdent(s)
17074                            if s.eq_ignore_ascii_case("virtual") =>
17075                        {
17076                            self.advance();
17077                            false
17078                        }
17079                        other => {
17080                            return Err(self.err(alloc::format!(
17081                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17082                                 got {other:?}"
17083                            )));
17084                        }
17085                    };
17086                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17087                    generated_stored_expr = Some(Box::new(expr));
17088                    continue;
17089                }
17090                self.expect_keyword_ident("identity")?;
17091                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17092                // consume the balanced parens and discard (SPG's
17093                // auto-increment is max+1-scan based).
17094                if matches!(self.peek(), Token::LParen) {
17095                    let mut depth = 0usize;
17096                    loop {
17097                        match self.advance() {
17098                            Token::LParen => depth += 1,
17099                            Token::RParen => {
17100                                depth -= 1;
17101                                if depth == 0 {
17102                                    break;
17103                                }
17104                            }
17105                            Token::Eof => {
17106                                return Err(self.err(
17107                                    "unterminated sequence-options parens after IDENTITY".into(),
17108                                ));
17109                            }
17110                            _ => {}
17111                        }
17112                    }
17113                }
17114                auto_increment = true;
17115                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17116                // can reject explicit non-DEFAULT INSERT values (unless
17117                // OVERRIDING SYSTEM VALUE) the way PG does.
17118                identity_always = saw_generated_always;
17119                // PG identity columns are implicitly NOT NULL.
17120                nullable = false;
17121                continue;
17122            }
17123            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17124            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17125            // is accepted today. The "ON" token is an Ident
17126            // (not reserved) — peek before consuming.
17127            if matches!(self.peek(), Token::On)
17128                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17129            {
17130                self.advance(); // ON
17131                self.advance(); // update
17132                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17133                let next = self.peek().clone();
17134                match next {
17135                    Token::Ident(s) | Token::QuotedIdent(s)
17136                        if s.eq_ignore_ascii_case("current_timestamp") =>
17137                    {
17138                        self.advance();
17139                        // Optional `(N)` precision.
17140                        if matches!(self.peek(), Token::LParen) {
17141                            self.advance();
17142                            if !matches!(self.peek(), Token::Integer(_)) {
17143                                return Err(self.err(alloc::format!(
17144                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17145                                    self.peek()
17146                                )));
17147                            }
17148                            self.advance();
17149                            if !matches!(self.peek(), Token::RParen) {
17150                                return Err(self.err(alloc::format!(
17151                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17152                                    self.peek()
17153                                )));
17154                            }
17155                            self.advance();
17156                        }
17157                        on_update_runtime = Some(Expr::FunctionCall {
17158                            name: "now".into(),
17159                            args: Vec::new(),
17160                        });
17161                        continue;
17162                    }
17163                    other => {
17164                        return Err(self.err(alloc::format!(
17165                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17166                        )));
17167                    }
17168                }
17169            }
17170            if matches!(self.peek(), Token::Default) {
17171                if default.is_some() {
17172                    return Err(self.err("DEFAULT specified twice".into()));
17173                }
17174                self.advance();
17175                default = Some(self.parse_expr(0)?);
17176                continue;
17177            }
17178            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17179            // token with NOT NULL and sits EARLIER in the loop than the
17180            // deferrability arm, so without the lookahead it was reported as
17181            // "NOT NULL specified twice" (or "expected NULL after NOT").
17182            if matches!(self.peek(), Token::Not)
17183                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17184            {
17185                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17186                self.consume_optional_deferrable_clauses()?;
17187                continue;
17188            }
17189            if matches!(self.peek(), Token::Not) {
17190                if nullability_seen {
17191                    return Err(self.err("NOT NULL specified twice".into()));
17192                }
17193                self.advance();
17194                if !matches!(self.peek(), Token::Null) {
17195                    return Err(self.err(format!(
17196                        "expected NULL after NOT in column def, got {:?}",
17197                        self.peek()
17198                    )));
17199                }
17200                self.advance();
17201                nullable = false;
17202                nullability_seen = true;
17203                continue;
17204            }
17205            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17206            // "this column is nullable" marker (the default in
17207            // standard SQL anyway). mysqldump emits it routinely
17208            // (`col TYPE NULL DEFAULT NULL` for nullable
17209            // timestamps etc). Accept + no-op.
17210            if matches!(self.peek(), Token::Null) {
17211                if nullability_seen && !nullable {
17212                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17213                    // sentence, PG18-measured (the table name is the
17214                    // caller's; the column half is exact).
17215                    return Err(self.err(alloc::format!(
17216                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17217                    )));
17218                }
17219                self.advance();
17220                nullable = true;
17221                nullability_seen = true;
17222                continue;
17223            }
17224            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17225            // arrives as a bare Ident. Match either, case-insensitive.
17226            if let Token::Ident(s) = self.peek()
17227                && (s.eq_ignore_ascii_case("auto_increment")
17228                    || s.eq_ignore_ascii_case("autoincrement"))
17229            {
17230                if auto_increment {
17231                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17232                }
17233                self.advance();
17234                auto_increment = true;
17235                continue;
17236            }
17237            // v7.9.13 — inline `PRIMARY KEY` column constraint
17238            // (mailrs F1). Implies `NOT NULL`. The engine creates
17239            // a BTree index for the PK column at CREATE TABLE time
17240            // so FK parent-side index lookups resolve.
17241            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17242            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17243            // spelling was a parse error, so a pg_dump carrying one stopped
17244            // mid-restore. The clauses are consumed by the same helper the FK
17245            // path has used since round 288 and recorded nowhere: SPG enforces
17246            // the constraint IMMEDIATELY either way, which fails earlier than
17247            // PG inside a transaction that violates-then-repairs — a refusal,
17248            // not a wrong answer. True deferral is the open remainder of F08.
17249            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17250                || (matches!(self.peek(), Token::Not)
17251                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17252            {
17253                // v7.39 (round 711) — CARRIED now (the storing half of
17254                // F08); round 621 only consumed.
17255                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17256                constraint_deferrable |= d;
17257                constraint_initially_deferred |= idef;
17258                continue;
17259            }
17260            if let Token::Ident(s) = self.peek()
17261                && s.eq_ignore_ascii_case("primary")
17262            {
17263                if is_primary_key {
17264                    return Err(self.err("PRIMARY KEY specified twice".into()));
17265                }
17266                // Peek-ahead for the required `KEY` token.
17267                let next = self.tokens.get(self.pos + 1);
17268                let next_is_key = matches!(
17269                    next,
17270                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17271                );
17272                if !next_is_key {
17273                    return Err(self.err(format!(
17274                        "expected KEY after PRIMARY in column def, got {:?}",
17275                        next
17276                    )));
17277                }
17278                self.advance(); // PRIMARY
17279                self.advance(); // KEY
17280                is_primary_key = true;
17281                if nullability_seen && nullable {
17282                    return Err(self.err(
17283                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17284                    ));
17285                }
17286                nullable = false;
17287                nullability_seen = true;
17288                continue;
17289            }
17290            // v7.13.0 — inline `UNIQUE` column constraint
17291            // (mailrs round-5 G2). Fold into a single-column
17292            // table-level UNIQUE at CREATE TABLE post-process time.
17293            if let Token::Ident(s) = self.peek()
17294                && s.eq_ignore_ascii_case("unique")
17295            {
17296                if is_unique {
17297                    return Err(self.err("UNIQUE specified twice".into()));
17298                }
17299                self.advance();
17300                is_unique = true;
17301                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17302                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17303                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17304                    let n1 = self.tokens.get(self.pos + 1);
17305                    let n2 = self.tokens.get(self.pos + 2);
17306                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17307                        self.advance(); // NULLS
17308                        self.advance(); // NOT
17309                        self.advance(); // DISTINCT
17310                        unique_nulls_not_distinct = true;
17311                    } else if matches!(n1, Some(Token::Distinct)) {
17312                        self.advance(); // NULLS
17313                        self.advance(); // DISTINCT
17314                    }
17315                }
17316                continue;
17317            }
17318            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17319            // (mailrs round-5 G3). PG semantics: column-level
17320            // CHECK is equivalent to a table-level CHECK. Multiple
17321            // inline CHECKs on the same column AND together.
17322            if let Token::Ident(s) = self.peek()
17323                && s.eq_ignore_ascii_case("check")
17324            {
17325                self.advance();
17326                if !matches!(self.peek(), Token::LParen) {
17327                    return Err(self.err(alloc::format!(
17328                        "expected '(' after CHECK in column def, got {:?}",
17329                        self.peek()
17330                    )));
17331                }
17332                self.advance();
17333                let pred = self.parse_expr(0)?;
17334                if !matches!(self.peek(), Token::RParen) {
17335                    return Err(self.err(alloc::format!(
17336                        "expected ')' to close CHECK predicate, got {:?}",
17337                        self.peek()
17338                    )));
17339                }
17340                self.advance();
17341                check = Some(match check.take() {
17342                    Some(prev) => Expr::Binary {
17343                        op: BinOp::And,
17344                        lhs: Box::new(prev),
17345                        rhs: Box::new(pred),
17346                    },
17347                    None => pred,
17348                });
17349                continue;
17350            }
17351            break;
17352        }
17353        Ok(ColumnDef {
17354            name,
17355            ty,
17356            nullable,
17357            default,
17358            auto_increment,
17359            is_primary_key,
17360            is_unique,
17361            unique_nulls_not_distinct,
17362            constraint_deferrable,
17363            constraint_initially_deferred,
17364            check,
17365            user_type_ref,
17366            on_update_runtime,
17367            collation,
17368            collation_explicit,
17369            collation_name,
17370            is_unsigned,
17371            inline_enum_variants,
17372            inline_set_variants,
17373            generated_stored_expr,
17374            identity_always,
17375            mysql_int_width,
17376            mysql_fsp,
17377        })
17378    }
17379
17380    /// `NUMERIC` may appear without parameters, with one (precision
17381    /// only, scale=0), or with both. Returns `(precision, scale)` with
17382    /// 0 = unspecified for the bare form.
17383    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17384        if !matches!(self.peek(), Token::LParen) {
17385            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17386            // we surface it as precision=0 to mean "unconstrained" so
17387            // the engine doesn't need a separate variant.
17388            return Ok((0, 0));
17389        }
17390        self.advance();
17391        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17392        // it words the out-of-range case with the value it saw. SPG
17393        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17394        // accepts failed to parse at all; values wider than i128 are
17395        // carried by the arbitrary-precision form.
17396        let precision = match self.advance() {
17397            Token::Integer(n) if (1..=1000).contains(&n) => {
17398                u16::try_from(n).expect("range-checked")
17399            }
17400            Token::Integer(n) => {
17401                return Err(ParseError {
17402                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17403                    token_pos: self.consumed_pos(),
17404                });
17405            }
17406            other => {
17407                return Err(ParseError {
17408                    message: format!(
17409                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17410                    ),
17411                    token_pos: self.consumed_pos(),
17412                });
17413            }
17414        };
17415        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17416        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17417        // then overflows). A negative scale rounds to tens / hundreds / …
17418        let scale = if matches!(self.peek(), Token::Comma) {
17419            self.advance();
17420            let neg = if matches!(self.peek(), Token::Minus) {
17421                self.advance();
17422                true
17423            } else {
17424                false
17425            };
17426            match self.advance() {
17427                Token::Integer(n) => {
17428                    let signed = if neg { -n } else { n };
17429                    if !(-1000..=1000).contains(&signed) {
17430                        return Err(ParseError {
17431                            message: format!(
17432                                "NUMERIC scale {signed} must be between -1000 and 1000"
17433                            ),
17434                            token_pos: self.consumed_pos(),
17435                        });
17436                    }
17437                    i16::try_from(signed).expect("range-checked")
17438                }
17439                other => {
17440                    return Err(ParseError {
17441                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17442                        token_pos: self.consumed_pos(),
17443                    });
17444                }
17445            }
17446        } else {
17447            0
17448        };
17449        if !matches!(self.peek(), Token::RParen) {
17450            return Err(self.err(format!(
17451                "expected ')' to close NUMERIC params, got {:?}",
17452                self.peek()
17453            )));
17454        }
17455        self.advance();
17456        Ok((precision, scale))
17457    }
17458
17459    /// Parse `(N)` where `N` is a positive integer literal — used by the
17460    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17461    /// for the error message.
17462    /// v6.0.1: parse the optional `USING <encoding>` clause that
17463    /// follows `VECTOR(N)` in a column definition. Missing clause
17464    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17465    /// ident → `ParseError` listing the encodings recognised today.
17466    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17467        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17468            return Ok(VecEncoding::F32);
17469        }
17470        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17471        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17472        // consume the token when the very next token is a known
17473        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17474        // `USING` for the caller — it's the rewrite-expression form.
17475        let n1 = self.tokens.get(self.pos + 1);
17476        let next_is_encoding = matches!(
17477            n1,
17478            Some(Token::Ident(s))
17479                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17480        );
17481        if !next_is_encoding {
17482            return Ok(VecEncoding::F32);
17483        }
17484        self.advance();
17485        let enc_ident = match self.advance() {
17486            Token::Ident(s) => s,
17487            other => {
17488                return Err(self.err(format!(
17489                    "expected vector encoding after USING, got {other:?}"
17490                )));
17491            }
17492        };
17493        match enc_ident.to_ascii_lowercase().as_str() {
17494            "sq8" => Ok(VecEncoding::Sq8),
17495            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17496            // binary16 per-element storage.
17497            "half" => Ok(VecEncoding::F16),
17498            other => Err(self.err(format!(
17499                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17500            ))),
17501        }
17502    }
17503
17504    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17505    /// without consuming it. Returns `Some(N)` when the next
17506    /// tokens are `( <int> )`; None otherwise. Used by the
17507    /// TINYINT classifier to decide whether to map to Bool or
17508    /// SmallInt.
17509    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17510        if !matches!(self.peek(), Token::LParen) {
17511            return None;
17512        }
17513        let next = self.tokens.get(self.pos + 1)?;
17514        let n = match next {
17515            Token::Integer(n) => *n,
17516            _ => return None,
17517        };
17518        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17519            return None;
17520        }
17521        Some(n)
17522    }
17523
17524    /// v7.14.0 — consume an optional MySQL display-width
17525    /// parenthesised number after an integer type, returning
17526    /// nothing. `TINYINT(1)` etc.
17527    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17528    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17529    fn peek_paren_has_comma(&self) -> bool {
17530        let mut i = self.pos + 1;
17531        let mut depth = 1usize;
17532        while depth > 0 {
17533            match self.tokens.get(i) {
17534                Some(Token::LParen) => depth += 1,
17535                Some(Token::RParen) => depth -= 1,
17536                Some(Token::Comma) if depth == 1 => return true,
17537                None | Some(Token::Eof) => return false,
17538                _ => {}
17539            }
17540            i += 1;
17541        }
17542        false
17543    }
17544
17545    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17546    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17547    /// fractional-seconds precision that drives write truncation and render
17548    /// padding, where `consume_optional_paren_size` throws it away.
17549    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17550    fn take_optional_paren_size(&mut self) -> Option<u8> {
17551        let Some(Token::Integer(n)) = self
17552            .tokens
17553            .get(self.pos + 1)
17554            .filter(|_| matches!(self.peek(), Token::LParen))
17555            .cloned()
17556        else {
17557            self.consume_optional_paren_size();
17558            return None;
17559        };
17560        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17561            self.consume_optional_paren_size();
17562            return None;
17563        }
17564        self.consume_optional_paren_size();
17565        u8::try_from(n).ok()
17566    }
17567
17568    fn consume_optional_paren_size(&mut self) {
17569        if !matches!(self.peek(), Token::LParen) {
17570            return;
17571        }
17572        self.advance();
17573        // Skip until matching RParen (allow nested or any tokens).
17574        let mut depth = 1usize;
17575        while depth > 0 {
17576            match self.peek() {
17577                Token::LParen => depth += 1,
17578                Token::RParen => depth -= 1,
17579                Token::Eof => return,
17580                _ => {}
17581            }
17582            self.advance();
17583        }
17584    }
17585
17586    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17587        if !matches!(self.peek(), Token::LParen) {
17588            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17589        }
17590        self.advance();
17591        let n = match self.advance() {
17592            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17593                message: format!("{label} size too large: {n}"),
17594                token_pos: self.consumed_pos(),
17595            })?,
17596            other => {
17597                return Err(ParseError {
17598                    message: format!("expected positive integer {label} size, got {other:?}"),
17599                    token_pos: self.consumed_pos(),
17600                });
17601            }
17602        };
17603        if !matches!(self.peek(), Token::RParen) {
17604            return Err(self.err(format!(
17605                "expected ')' after {label} size, got {:?}",
17606                self.peek()
17607            )));
17608        }
17609        self.advance();
17610        Ok(n)
17611    }
17612
17613    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17614    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17615    /// key, like MySQL) whose action skips conflicting rows.
17616    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17617    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17618    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17619    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17620    /// common bulk-upsert spellings —
17621    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17622    ///     REPLACE INTO t SELECT …
17623    /// — were a parse error / a duplicate-key failure respectively.
17624    ///
17625    /// Precedence: an explicitly written clause beats a statement-level flag.
17626    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17627    /// implicit `REPLACE` and `IGNORE` lowerings.
17628    fn parse_insert_conflict_clause(
17629        &mut self,
17630        replace: bool,
17631        ignore: bool,
17632    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17633        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17634            return Ok(Some(c));
17635        }
17636        if let Some(c) = self.parse_optional_on_conflict()? {
17637            return Ok(Some(c));
17638        }
17639        if replace {
17640            // REPLACE INTO = delete-then-insert, which PG spells as
17641            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17642            // reads an empty assignment list as "take the incoming row".
17643            return Ok(Some(crate::ast::OnConflictClause {
17644                target_columns: Vec::new(),
17645                index_where: None,
17646                constraint_name: None,
17647                mysql_lowered: true,
17648                action: crate::ast::OnConflictAction::Update {
17649                    assignments: Vec::new(),
17650                    where_: None,
17651                },
17652            }));
17653        }
17654        if ignore {
17655            return Ok(Some(Self::insert_ignore_clause()));
17656        }
17657        Ok(None)
17658    }
17659
17660    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17661    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17662    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17663    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17664    fn parse_optional_on_duplicate_key(
17665        &mut self,
17666    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17667        if !(matches!(self.peek(), Token::On)
17668            && matches!(self.tokens.get(self.pos + 1),
17669                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17670        {
17671            return Ok(None);
17672        }
17673        self.advance(); // ON
17674        self.advance(); // DUPLICATE
17675        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17676            return Err(self.err(format!(
17677                "expected KEY after ON DUPLICATE, got {:?}",
17678                self.peek()
17679            )));
17680        }
17681        self.advance();
17682        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17683            return Err(self.err(format!(
17684                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17685                self.peek()
17686            )));
17687        }
17688        self.advance();
17689        let mut assignments: Vec<(String, Expr)> = Vec::new();
17690        loop {
17691            let col = self.expect_ident_like()?;
17692            if !matches!(self.peek(), Token::Eq) {
17693                return Err(self.err(format!(
17694                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17695                    self.peek()
17696                )));
17697            }
17698            self.advance();
17699            let mut expr = self.parse_expr(0)?;
17700            Self::rewrite_mysql_values_refs(&mut expr);
17701            assignments.push((col, expr));
17702            if matches!(self.peek(), Token::Comma) {
17703                self.advance();
17704                continue;
17705            }
17706            break;
17707        }
17708        Ok(Some(crate::ast::OnConflictClause {
17709            target_columns: Vec::new(),
17710            index_where: None,
17711            constraint_name: None,
17712            mysql_lowered: true,
17713            action: crate::ast::OnConflictAction::Update {
17714                assignments,
17715                where_: None,
17716            },
17717        }))
17718    }
17719
17720    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17721        crate::ast::OnConflictClause {
17722            target_columns: Vec::new(),
17723            index_where: None,
17724            constraint_name: None,
17725            mysql_lowered: true,
17726            action: crate::ast::OnConflictAction::Nothing,
17727        }
17728    }
17729
17730    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17731        debug_assert!(
17732            matches!(self.peek(), Token::Insert)
17733                || (replace
17734                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17735        );
17736        self.advance();
17737        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17738        // would raise a duplicate-key error instead of failing the statement,
17739        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17740        // plain ident to the lexer; only the MySQL dialect accepts it here.
17741        let ignore = self.mysql_dialect
17742            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17743        if ignore {
17744            self.advance();
17745        }
17746        if !matches!(self.peek(), Token::Into) {
17747            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17748        }
17749        self.advance();
17750        let table = self.expect_ident_like()?;
17751        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17752        // grammar requires the AS keyword here (a bare identifier would be
17753        // ambiguous with a column list). The alias is what the ON CONFLICT
17754        // DO UPDATE expressions refer to the target row by.
17755        let alias = if matches!(self.peek(), Token::As) {
17756            self.advance();
17757            Some(self.expect_ident_like()?)
17758        } else {
17759            None
17760        };
17761        // v7.39 (round 428) — MySQL's SET-form INSERT:
17762        //     INSERT INTO t SET a = 1, b = 'x'
17763        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17764        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17765        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17766        // measured). So it lowers to the column list + one VALUES row and
17767        // rejoins the ordinary path, which already handles every one of
17768        // those. PG has no such spelling, hence the dialect gate.
17769        if self.mysql_dialect
17770            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17771        {
17772            self.advance(); // SET
17773            let mut names = Vec::new();
17774            let mut values = Vec::new();
17775            loop {
17776                names.push(self.expect_ident_like()?);
17777                if !matches!(self.peek(), Token::Eq) {
17778                    return Err(self.err(alloc::format!(
17779                        "expected '=' in INSERT … SET, got {:?}",
17780                        self.peek()
17781                    )));
17782                }
17783                self.advance();
17784                // `SET a = DEFAULT` rides the same `__column_default` marker
17785                // the VALUES-row and UPDATE-SET paths use; the INSERT
17786                // executor resolves it against the target column.
17787                if matches!(self.peek(), Token::Default) {
17788                    self.advance();
17789                    values.push(Expr::FunctionCall {
17790                        name: "__column_default".to_string(),
17791                        args: Vec::new(),
17792                    });
17793                } else {
17794                    values.push(self.parse_expr(0)?);
17795                }
17796                if matches!(self.peek(), Token::Comma) {
17797                    self.advance();
17798                    continue;
17799                }
17800                break;
17801            }
17802            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17803            let returning = self.parse_optional_returning()?;
17804            return Ok(Statement::Insert(InsertStatement {
17805                ctes: Vec::new(),
17806                table,
17807                alias,
17808                columns: Some(names),
17809                rows: alloc::vec![values],
17810                select_source: None,
17811                // MySQL's SET form has no `OVERRIDING …` clause (that is
17812                // PG's identity-column spelling).
17813                overriding: Overriding::None,
17814                mysql_ignore: ignore,
17815                on_conflict,
17816                returning,
17817            }));
17818        }
17819        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17820        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17821        // a parenthesized query source instead (PG select_with_parens:
17822        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17823        // both keywords are reserved in PG, so no column list can start
17824        // with them.
17825        let columns = if matches!(self.peek(), Token::LParen) {
17826            self.advance();
17827            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17828                let select_stmt = if self.peek_is_with_kw() {
17829                    self.advance();
17830                    self.parse_nested_with_select()?
17831                } else {
17832                    match self.parse_select_stmt()? {
17833                        Statement::Select(s) => s,
17834                        other => {
17835                            return Err(self.err(alloc::format!(
17836                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17837                            )));
17838                        }
17839                    }
17840                };
17841                if !matches!(self.peek(), Token::RParen) {
17842                    return Err(self.err(format!(
17843                        "expected ')' after parenthesized INSERT source, got {:?}",
17844                        self.peek()
17845                    )));
17846                }
17847                self.advance();
17848                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17849                let returning = self.parse_optional_returning()?;
17850                return Ok(Statement::Insert(InsertStatement {
17851                    ctes: Vec::new(),
17852                    table,
17853                    alias: alias.clone(),
17854                    columns: None,
17855                    rows: Vec::new(),
17856                    select_source: Some(Box::new(select_stmt)),
17857                    on_conflict,
17858                    returning,
17859                    overriding: Overriding::None,
17860                    mysql_ignore: ignore,
17861                }));
17862            }
17863            let mut names = Vec::new();
17864            loop {
17865                names.push(self.expect_ident_like()?);
17866                match self.peek() {
17867                    Token::Comma => {
17868                        self.advance();
17869                    }
17870                    Token::RParen => {
17871                        self.advance();
17872                        break;
17873                    }
17874                    other => {
17875                        return Err(self.err(format!(
17876                            "expected ',' or ')' in INSERT column list, got {other:?}"
17877                        )));
17878                    }
17879                }
17880            }
17881            Some(names)
17882        } else {
17883            None
17884        };
17885        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17886        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17887        // is captured on the statement so the engine can apply PG's
17888        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17889        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17890        {
17891            self.advance();
17892            let which = self.expect_ident_like()?;
17893            let ov = if which.eq_ignore_ascii_case("system") {
17894                Overriding::System
17895            } else if which.eq_ignore_ascii_case("user") {
17896                Overriding::User
17897            } else {
17898                return Err(self.err(format!(
17899                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17900                )));
17901            };
17902            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17903                return Err(self.err(format!(
17904                    "expected VALUE after OVERRIDING {}, got {:?}",
17905                    which.to_ascii_uppercase(),
17906                    self.peek()
17907                )));
17908            }
17909            self.advance();
17910            ov
17911        } else {
17912            Overriding::None
17913        };
17914        // `INSERT INTO t DEFAULT VALUES` — a single row made
17915        // entirely of column defaults. Lower to the permuted
17916        // column-list path with an empty list: every schema column
17917        // is unmapped, so the engine fills each from its default
17918        // (serials advance, plain defaults evaluate, the rest NULL).
17919        if matches!(self.peek(), Token::Default) {
17920            self.advance();
17921            if !matches!(self.peek(), Token::Values) {
17922                return Err(self.err(format!(
17923                    "expected VALUES after DEFAULT in INSERT, got {:?}",
17924                    self.peek()
17925                )));
17926            }
17927            self.advance();
17928            if columns.is_some() {
17929                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17930            }
17931            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17932            let returning = self.parse_optional_returning()?;
17933            return Ok(Statement::Insert(InsertStatement {
17934                ctes: Vec::new(),
17935                table,
17936                alias: alias.clone(),
17937                columns: Some(Vec::new()),
17938                rows: alloc::vec![Vec::new()],
17939                select_source: None,
17940                on_conflict,
17941                returning,
17942                overriding,
17943                mysql_ignore: ignore,
17944            }));
17945        }
17946        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17947        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17948        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17949        // SELECT …`) heads the SOURCE select, as in PG (the statement's
17950        // own WITH comes before INSERT).
17951        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17952            let select_stmt = if self.peek_is_with_kw() {
17953                self.advance();
17954                self.parse_nested_with_select()?
17955            } else {
17956                match self.parse_select_stmt()? {
17957                    Statement::Select(s) => s,
17958                    other => {
17959                        return Err(self.err(alloc::format!(
17960                            "expected SELECT after INSERT INTO ... target, got {other:?}"
17961                        )));
17962                    }
17963                }
17964            };
17965            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17966            let returning = self.parse_optional_returning()?;
17967            return Ok(Statement::Insert(InsertStatement {
17968                ctes: Vec::new(),
17969                table,
17970                alias: alias.clone(),
17971                columns,
17972                rows: Vec::new(),
17973                select_source: Some(Box::new(select_stmt)),
17974                on_conflict,
17975                returning,
17976                overriding,
17977                mysql_ignore: ignore,
17978            }));
17979        }
17980        if !matches!(self.peek(), Token::Values) {
17981            return Err(self.err(format!(
17982                "expected VALUES or SELECT after table name, got {:?}",
17983                self.peek()
17984            )));
17985        }
17986        self.advance();
17987        if !matches!(self.peek(), Token::LParen) {
17988            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17989        }
17990        let mut rows = Vec::new();
17991        loop {
17992            // Each iteration consumes one `(expr, expr, …)` tuple.
17993            if !matches!(self.peek(), Token::LParen) {
17994                return Err(self.err(format!(
17995                    "expected '(' for next VALUES tuple, got {:?}",
17996                    self.peek()
17997                )));
17998            }
17999            self.advance();
18000            let mut tuple = Vec::new();
18001            loop {
18002                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18003                // the column's declared default for that slot. Rides out as the
18004                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18005                // path uses; the INSERT executor resolves it per target column.
18006                if matches!(self.peek(), Token::Default) {
18007                    self.advance();
18008                    tuple.push(Expr::FunctionCall {
18009                        name: "__column_default".to_string(),
18010                        args: Vec::new(),
18011                    });
18012                } else {
18013                    tuple.push(self.parse_expr(0)?);
18014                }
18015                match self.peek() {
18016                    Token::Comma => {
18017                        self.advance();
18018                    }
18019                    Token::RParen => {
18020                        self.advance();
18021                        break;
18022                    }
18023                    other => {
18024                        return Err(self.err(format!(
18025                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18026                        )));
18027                    }
18028                }
18029            }
18030            if tuple.is_empty() {
18031                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18032            }
18033            rows.push(tuple);
18034            // Continue with comma-separated tuples.
18035            if matches!(self.peek(), Token::Comma) {
18036                self.advance();
18037            } else {
18038                break;
18039            }
18040        }
18041        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18042        // to ON CONFLICT DO UPDATE with an empty conflict target
18043        // (the engine picks the table's first unique index, which
18044        // matches MySQL's any-unique-key behaviour for the common
18045        // single-key case). `VALUES(col)` in the assignments is
18046        // MySQL's spelling of EXCLUDED.col.
18047        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18048        let returning = self.parse_optional_returning()?;
18049        Ok(Statement::Insert(InsertStatement {
18050            ctes: Vec::new(),
18051            table,
18052            alias,
18053            columns,
18054            rows,
18055            select_source: None,
18056            on_conflict,
18057            returning,
18058            overriding,
18059            mysql_ignore: ignore,
18060        }))
18061    }
18062
18063    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18064    /// the incoming row's value — exactly PG's EXCLUDED.col.
18065    fn rewrite_mysql_values_refs(e: &mut Expr) {
18066        match e {
18067            Expr::FunctionCall { name, args }
18068                if name.eq_ignore_ascii_case("values")
18069                    && args.len() == 1
18070                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18071            {
18072                let Expr::Column(c) = &args[0] else {
18073                    unreachable!("guarded above");
18074                };
18075                *e = Expr::Column(crate::ast::ColumnName {
18076                    qualifier: Some("EXCLUDED".to_string()),
18077                    name: c.name.clone(),
18078                });
18079            }
18080            Expr::FunctionCall { args, .. } => {
18081                for a in args {
18082                    Self::rewrite_mysql_values_refs(a);
18083                }
18084            }
18085            Expr::Binary { lhs, rhs, .. } => {
18086                Self::rewrite_mysql_values_refs(lhs);
18087                Self::rewrite_mysql_values_refs(rhs);
18088            }
18089            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18090                Self::rewrite_mysql_values_refs(expr);
18091            }
18092            Expr::Case {
18093                operand,
18094                branches,
18095                else_branch,
18096            } => {
18097                if let Some(op) = operand {
18098                    Self::rewrite_mysql_values_refs(op);
18099                }
18100                for (w, t) in branches {
18101                    Self::rewrite_mysql_values_refs(w);
18102                    Self::rewrite_mysql_values_refs(t);
18103                }
18104                if let Some(el) = else_branch {
18105                    Self::rewrite_mysql_values_refs(el);
18106                }
18107            }
18108            _ => {}
18109        }
18110    }
18111
18112    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18113    /// clause sitting between the INSERT body and the trailing
18114    /// RETURNING. All keywords come in as bare idents; `ON` is
18115    /// a reserved Token though.
18116    fn parse_optional_on_conflict(
18117        &mut self,
18118    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18119        if !matches!(self.peek(), Token::On) {
18120            return Ok(None);
18121        }
18122        // Peek further: we want exactly "ON CONFLICT ...". If the
18123        // next ident isn't "conflict", let some other parser handle.
18124        let next_is_conflict = matches!(
18125            self.tokens.get(self.pos + 1),
18126            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18127        );
18128        if !next_is_conflict {
18129            return Ok(None);
18130        }
18131        self.advance(); // ON
18132        self.advance(); // CONFLICT
18133        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18134        // the constraint instead of listing columns (the pg_dump
18135        // form); the engine resolves it.
18136        let mut constraint_name: Option<String> = None;
18137        if matches!(self.peek(), Token::On) {
18138            self.advance(); // ON
18139            match self.advance() {
18140                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18141                }
18142                other => {
18143                    return Err(self.err(alloc::format!(
18144                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18145                    )));
18146                }
18147            }
18148            constraint_name = Some(self.expect_ident_like()?);
18149        }
18150        // Optional `(col [, col]*)` target list.
18151        let mut target_columns: Vec<String> = Vec::new();
18152        if matches!(self.peek(), Token::LParen) {
18153            self.advance();
18154            loop {
18155                target_columns.push(self.expect_ident_like()?);
18156                match self.peek() {
18157                    Token::Comma => {
18158                        self.advance();
18159                    }
18160                    Token::RParen => {
18161                        self.advance();
18162                        break;
18163                    }
18164                    other => {
18165                        return Err(self.err(alloc::format!(
18166                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18167                        )));
18168                    }
18169                }
18170            }
18171        }
18172        // v7.39 (round 240) — optional index predicate after the target
18173        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18174        // PARTIAL unique index; SPG's arbiters are full indexes, which
18175        // satisfy any predicate, so it is parsed and carried but not
18176        // consulted (recorded residual: partial-unique-index arbiters).
18177        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18178            self.advance();
18179            Some(self.parse_expr(0)?)
18180        } else {
18181            None
18182        };
18183        // Required `DO`.
18184        match self.advance() {
18185            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18186            other => {
18187                return Err(self.err(alloc::format!(
18188                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18189                )));
18190            }
18191        }
18192        // Action: NOTHING | UPDATE SET …
18193        let action = match self.advance() {
18194            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18195                crate::ast::OnConflictAction::Nothing
18196            }
18197            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18198                self.parse_on_conflict_update_action()?
18199            }
18200            other => {
18201                return Err(self.err(alloc::format!(
18202                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18203                )));
18204            }
18205        };
18206        Ok(Some(crate::ast::OnConflictClause {
18207            target_columns,
18208            index_where,
18209            constraint_name,
18210            mysql_lowered: false,
18211            action,
18212        }))
18213    }
18214
18215    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18216    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18217    /// consumed `UPDATE`.
18218    fn parse_on_conflict_update_action(
18219        &mut self,
18220    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18221        // `SET`
18222        match self.advance() {
18223            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18224            other => {
18225                return Err(self.err(alloc::format!(
18226                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18227                )));
18228            }
18229        }
18230        let mut assignments: Vec<(String, Expr)> = Vec::new();
18231        loop {
18232            let col = self.expect_ident_like()?;
18233            if !matches!(self.peek(), Token::Eq) {
18234                return Err(self.err(alloc::format!(
18235                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18236                    self.peek()
18237                )));
18238            }
18239            self.advance();
18240            let value = self.parse_expr(0)?;
18241            assignments.push((col, value));
18242            if matches!(self.peek(), Token::Comma) {
18243                self.advance();
18244                continue;
18245            }
18246            break;
18247        }
18248        let where_ = if matches!(self.peek(), Token::Where) {
18249            self.advance();
18250            Some(self.parse_expr(0)?)
18251        } else {
18252            None
18253        };
18254        Ok(crate::ast::OnConflictAction::Update {
18255            assignments,
18256            where_,
18257        })
18258    }
18259
18260    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18261        let mut items = Vec::new();
18262        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18263        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18264        // answers one zero-column row per row of t, and a bare `SELECT`
18265        // answers a single zero-column row. SPG required at least one
18266        // item, so both were syntax errors. Recognised by the token that
18267        // follows — nothing that can start an expression appears here.
18268        if self.select_list_is_empty_here() {
18269            return Ok(items);
18270        }
18271        loop {
18272            items.push(self.parse_select_item()?);
18273            if matches!(self.peek(), Token::Comma) {
18274                self.advance();
18275            } else {
18276                break;
18277            }
18278        }
18279        Ok(items)
18280    }
18281
18282    /// Is the target list empty at this point — i.e. does the next token
18283    /// end the SELECT's item list rather than start an item?
18284    fn select_list_is_empty_here(&self) -> bool {
18285        match self.peek() {
18286            Token::From
18287            | Token::Where
18288            | Token::Group
18289            | Token::Having
18290            | Token::Order
18291            | Token::Limit
18292            | Token::Offset
18293            | Token::Semicolon
18294            | Token::RParen
18295            | Token::Union
18296            | Token::Except
18297            | Token::Eof => true,
18298            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18299            // with unreserved keywords, so they arrive as plain idents.
18300            Token::Ident(s) => {
18301                s.eq_ignore_ascii_case("fetch")
18302                    || s.eq_ignore_ascii_case("window")
18303                    || s.eq_ignore_ascii_case("intersect")
18304            }
18305            _ => false,
18306        }
18307    }
18308
18309    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18310        if matches!(self.peek(), Token::Star) {
18311            self.advance();
18312            return Ok(SelectItem::Wildcard);
18313        }
18314        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18315        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18316        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18317        // `<ident> . *` with nothing binding tighter.
18318        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18319            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18320                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18321            {
18322                self.advance(); // qualifier
18323                self.advance(); // .
18324                self.advance(); // *
18325                return Ok(SelectItem::QualifiedWildcard(q));
18326            }
18327        }
18328        let start_tok = self.pos;
18329        let expr = self.parse_expr(0)?;
18330        let end_tok = self.consumed_pos();
18331        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18332        // multi-column function returns into columns. Marked here and lowered in
18333        // `parse_bare_select`, where the FROM clause is in hand.
18334        if matches!(self.peek(), Token::Dot)
18335            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18336        {
18337            self.advance(); // .
18338            self.advance(); // *
18339            return Ok(SelectItem::Expr {
18340                expr: Expr::FunctionCall {
18341                    name: "__record_expand".to_string(),
18342                    args: alloc::vec![expr],
18343                },
18344                alias: None,
18345            });
18346        }
18347        let alias = match self.parse_optional_alias()? {
18348            Some(a) => Some(a),
18349            None => self.mysql_item_label(&expr, start_tok, end_tok),
18350        };
18351        Ok(SelectItem::Expr { expr, alias })
18352    }
18353
18354    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18355    /// carries no `AS`, filled in here so every downstream path reports it
18356    /// without knowing the rule. `None` leaves the item un-aliased, which is
18357    /// what a PG session always gets.
18358    ///
18359    /// Measured against MariaDB 11, three rules and no more:
18360    ///
18361    /// | item             | label      | why                          |
18362    /// |------------------|------------|------------------------------|
18363    /// | `lbl.a`          | `a`        | a column reports its name    |
18364    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18365    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18366    ///
18367    /// The third is why this lives in the parser at all: the label is the
18368    /// text the client WROTE, down to the spacing, so it cannot be printed
18369    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18370    ///
18371    /// Comments survive, and that is right: through a `mariadb` CLI both
18372    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18373    /// CLIENT stripping the comment before it sends. Asked over the raw
18374    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18375    /// produces.
18376    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18377        if !self.mysql_dialect {
18378            return None;
18379        }
18380        match expr {
18381            // A column already reports its own name downstream; naming it
18382            // again here would only re-state the qualifier the label drops.
18383            Expr::Column(_) => None,
18384            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18385            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18386        }
18387    }
18388
18389    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18390    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18391    /// with PG's default column1..columnN names; subsequent rows
18392    /// chain as UNION ALL peers. Shared by the FROM-position
18393    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18394    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18395        let mut row_selects: Vec<SelectStatement> = Vec::new();
18396        loop {
18397            if !matches!(self.peek(), Token::LParen) {
18398                return Err(self.err(alloc::format!(
18399                    "expected '(' to start a VALUES row, got {:?}",
18400                    self.peek()
18401                )));
18402            }
18403            self.advance(); // (
18404            let mut items: Vec<SelectItem> = Vec::new();
18405            loop {
18406                let expr = self.parse_expr(0)?;
18407                items.push(SelectItem::Expr {
18408                    expr,
18409                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18410                });
18411                match self.peek() {
18412                    Token::Comma => {
18413                        self.advance();
18414                    }
18415                    Token::RParen => break,
18416                    other => {
18417                        return Err(self.err(alloc::format!(
18418                            "expected ',' or ')' in VALUES row, got {other:?}"
18419                        )));
18420                    }
18421                }
18422            }
18423            self.advance(); // )
18424            row_selects.push(SelectStatement {
18425                locking: None,
18426                ctes: Vec::new(),
18427                distinct: false,
18428                distinct_on: Vec::new(),
18429                items,
18430                from: None,
18431                where_: None,
18432                group_by: None,
18433                group_by_all: false,
18434                having: None,
18435                unions: Vec::new(),
18436                order_by: Vec::new(),
18437                limit: None,
18438                offset: None,
18439                limit_with_ties: false,
18440                window_check_exprs: Vec::new(),
18441            });
18442            if matches!(self.peek(), Token::Comma) {
18443                self.advance();
18444                continue;
18445            }
18446            break;
18447        }
18448        let mut head = row_selects.remove(0);
18449        head.unions = row_selects
18450            .into_iter()
18451            .map(|s| (UnionKind::All, s))
18452            .collect();
18453        Ok(head)
18454    }
18455
18456    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18457        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18458        // children. It was read as a table NAMED `only`, so the query
18459        // failed on `relation "only" does not exist`.
18460        //
18461        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18462        // absorbed the keyword, reasoning that SPG's children are
18463        // separate relations a plain scan does not descend into, so ONLY
18464        // already described the scan. That stopped being true when a
18465        // partition parent started unioning its children: measured,
18466        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18467        // where PG answers 0. The flag is carried now.
18468        let mut only = false;
18469        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18470            && matches!(
18471                self.tokens.get(self.pos + 1),
18472                Some(Token::Ident(_) | Token::QuotedIdent(_))
18473            )
18474        {
18475            only = true;
18476            self.advance();
18477        }
18478        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18479        // for these SRFs the keyword is noise at parse time: the
18480        // join executor already substitutes outer-column references
18481        // into unnest_expr / generate_series_args per outer row
18482        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18483        // licences the correlation even without the keyword. Absorb
18484        // it and fall through to the SRF arms below.
18485        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18486        // just the four builtin SRFs: a user set-returning function on a JOIN's
18487        // right side is the whole point of LATERAL. The keyword stays noise at
18488        // parse time — the join executor substitutes the outer row into the
18489        // call's arguments per outer row.
18490        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18491            && matches!(
18492                self.tokens.get(self.pos + 1),
18493                // The json_each family has its OWN `LATERAL …` arm below, which
18494                // needs to see the keyword — absorbing it here would send those
18495                // calls down the generic table-function channel instead.
18496                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18497            )
18498            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18499        {
18500            self.advance(); // LATERAL
18501        }
18502        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18503        // set-returning function whose argument may reference a
18504        // preceding FROM item. We rewrite this to
18505        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18506        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18507        // executor handles per-outer-row evaluation and the
18508        // SRF-primary jsonb_each_text path handles the inner
18509        // materialisation. Sentori 0067 backfill is the dogfood
18510        // shape.
18511        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18512            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18513            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18514        {
18515            self.advance(); // LATERAL
18516            let each_fn = match self.peek() {
18517                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18518                _ => unreachable!(),
18519            };
18520            self.advance(); // jsonb_each[_text] / json_each[_text]
18521            self.advance(); // (
18522            let arg = self.parse_expr(0)?;
18523            if !matches!(self.peek(), Token::RParen) {
18524                return Err(self.err(alloc::format!(
18525                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18526                    self.peek()
18527                )));
18528            }
18529            self.advance();
18530            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18531            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18532            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18533            //               FROM jsonb_each_text(<arg>) AS __srf__
18534            // PG's `AS kv(key, value)` column-alias list maps
18535            // positions to names; default to (key, value) when
18536            // omitted (matching the SRF's natural column names).
18537            let srf_alias = "__srf__".to_string();
18538            let key_alias = column_aliases
18539                .first()
18540                .cloned()
18541                .unwrap_or_else(|| "key".to_string());
18542            let value_alias = column_aliases
18543                .get(1)
18544                .cloned()
18545                .unwrap_or_else(|| "value".to_string());
18546            let inner_select = crate::ast::SelectStatement {
18547                locking: None,
18548                ctes: Vec::new(),
18549                distinct: false,
18550                distinct_on: Vec::new(),
18551                items: alloc::vec![
18552                    crate::ast::SelectItem::Expr {
18553                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18554                            qualifier: Some(srf_alias.clone()),
18555                            name: "key".to_string(),
18556                        }),
18557                        alias: Some(key_alias),
18558                    },
18559                    crate::ast::SelectItem::Expr {
18560                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18561                            qualifier: Some(srf_alias.clone()),
18562                            name: "value".to_string(),
18563                        }),
18564                        alias: Some(value_alias),
18565                    },
18566                ],
18567                from: Some(crate::ast::FromClause {
18568                    primary: TableRef {
18569                        name: srf_alias.clone(),
18570                        alias: Some(srf_alias.clone()),
18571                        only: false,
18572                        as_of_segment: None,
18573                        unnest_expr: None,
18574                        unnest_column_aliases: Vec::new(),
18575                        with_ordinality: false,
18576                        generate_series_args: None,
18577                        lateral_subquery: None,
18578                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18579                        table_fn_call: None,
18580                        rows_from: None,
18581                        json_table: None,
18582                        scalar_fn_item: false,
18583                    },
18584                    joins: Vec::new(),
18585                }),
18586                where_: None,
18587                group_by: None,
18588                group_by_all: false,
18589                having: None,
18590                unions: Vec::new(),
18591                order_by: Vec::new(),
18592                limit: None,
18593                offset: None,
18594                limit_with_ties: false,
18595                window_check_exprs: Vec::new(),
18596            };
18597            return Ok(TableRef {
18598                name: alias.clone(),
18599                alias: Some(alias),
18600                only: false,
18601                as_of_segment: None,
18602                unnest_expr: None,
18603                unnest_column_aliases: Vec::new(),
18604                with_ordinality: false,
18605                generate_series_args: None,
18606                lateral_subquery: Some(Box::new(inner_select)),
18607                jsonb_each_text_arg: None,
18608                table_fn_call: None,
18609                rows_from: None,
18610                json_table: None,
18611                scalar_fn_item: false,
18612            });
18613        }
18614        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18615        // without an explicit `LATERAL` keyword is the same shape
18616        // PG accepts (SRF naturally licences lateral correlation).
18617        // We mirror the LATERAL rewrite when the argument syntactic-
18618        // ally references an outer column (Column { qualifier:
18619        // Some(_), … }). For simplicity we apply the rewrite
18620        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18621        // in the FROM-list — caller-side join parsing positions
18622        // this peek correctly.
18623        // (Implementation note: detection lives below; the LATERAL
18624        // branch above already covers the explicit form; the bare
18625        // form falls through to the plain SRF arm and the engine
18626        // treats it as a constant-arg SRF if no outer reference is
18627        // present.)
18628        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18629        // table. Detect at the head so it claims precedence over
18630        // every other table-ref shape (unnest / generate_series /
18631        // bare ident); the lateral subquery itself follows the
18632        // regular SELECT grammar.
18633        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18634        // t(cols)`. Each row lowers to a constant SELECT with PG's
18635        // default column1..columnN names; subsequent rows chain as
18636        // UNION ALL peers. The result rides the derived-table
18637        // lateral_subquery channel — zero executor work.
18638        if matches!(self.peek(), Token::LParen)
18639            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18640        {
18641            self.advance(); // (
18642            self.advance(); // VALUES
18643            let head = self.parse_values_rows_body()?;
18644            if !matches!(self.peek(), Token::RParen) {
18645                return Err(self.err(alloc::format!(
18646                    "expected ')' after VALUES list, got {:?}",
18647                    self.peek()
18648                )));
18649            }
18650            self.advance();
18651            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18652            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18653            return Ok(TableRef {
18654                name,
18655                alias: alias_ident,
18656                only: false,
18657                as_of_segment: None,
18658                unnest_expr: None,
18659                unnest_column_aliases: column_aliases,
18660                with_ordinality: false,
18661                generate_series_args: None,
18662                lateral_subquery: Some(Box::new(head)),
18663                jsonb_each_text_arg: None,
18664                table_fn_call: None,
18665                rows_from: None,
18666                json_table: None,
18667                scalar_fn_item: false,
18668            });
18669        }
18670        // v7.37.17 (17.6 siblings) — plain derived table:
18671        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18672        // lateral_subquery channel the explicit LATERAL form uses —
18673        // an uncorrelated inner SELECT executes identically. The
18674        // inner parse carries UNION tails (they live on
18675        // SelectStatement.unions).
18676        // v7.37 D.20 — the derived-table inner may itself be a
18677        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18678        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18679        // bare `(SELECT …)`. parse_one_statement already routes a leading
18680        // `(` set-op group (its LParen arm) and a leading WITH
18681        // (parse_with_cte_then_select), so widen the second-token gate to
18682        // Select | LParen | WITH.
18683        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18684        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18685        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18686        // has existed since the shorthand landed and `parse_bare_select`
18687        // already routes it ("valid anywhere a SELECT head is"); what was
18688        // missing is this second-token gate, and the CTE body's dispatch
18689        // below. Round 868 found both by putting the shorthand in a
18690        // subquery — the top-level forms had been the only ones tested.
18691        if matches!(self.peek(), Token::LParen)
18692            && (matches!(
18693                self.tokens.get(self.pos + 1),
18694                Some(Token::Select | Token::LParen | Token::Table)
18695            ) || matches!(self.tokens.get(self.pos + 1),
18696                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18697        {
18698            self.advance(); // (
18699            let inner = match self.parse_one_statement()? {
18700                Statement::Select(s) => s,
18701                other => {
18702                    return Err(self.err(alloc::format!(
18703                        "expected SELECT inside derived table ( … ), got {other:?}"
18704                    )));
18705                }
18706            };
18707            if !matches!(self.peek(), Token::RParen) {
18708                return Err(self.err(alloc::format!(
18709                    "expected ')' after derived-table subquery, got {:?}",
18710                    self.peek()
18711                )));
18712            }
18713            self.advance();
18714            // `AS t(a, b)` column-alias list rides the
18715            // unnest_column_aliases field (same positional-rename
18716            // contract the unnest SRFs use).
18717            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18718            let name = alias_ident
18719                .clone()
18720                .unwrap_or_else(|| "subquery".to_string());
18721            return Ok(TableRef {
18722                name,
18723                alias: alias_ident,
18724                only: false,
18725                as_of_segment: None,
18726                unnest_expr: None,
18727                unnest_column_aliases: column_aliases,
18728                with_ordinality: false,
18729                generate_series_args: None,
18730                lateral_subquery: Some(Box::new(inner)),
18731                jsonb_each_text_arg: None,
18732                table_fn_call: None,
18733                rows_from: None,
18734                json_table: None,
18735                scalar_fn_item: false,
18736            });
18737        }
18738        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18739            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18740        {
18741            self.advance(); // LATERAL
18742            self.advance(); // (
18743            // Parse the inner SELECT.
18744            let inner = match self.parse_one_statement()? {
18745                Statement::Select(s) => s,
18746                other => {
18747                    return Err(self.err(alloc::format!(
18748                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18749                    )));
18750                }
18751            };
18752            if !matches!(self.peek(), Token::RParen) {
18753                return Err(self.err(alloc::format!(
18754                    "expected ')' after LATERAL subquery, got {:?}",
18755                    self.peek()
18756                )));
18757            }
18758            self.advance();
18759            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18760            // `(VALUES …) t(g)` derived table round-trips through view-body
18761            // Display, which renders on the lateral_subquery channel).
18762            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18763            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18764            return Ok(TableRef {
18765                name,
18766                alias: alias_ident,
18767                only: false,
18768                as_of_segment: None,
18769                unnest_expr: None,
18770                unnest_column_aliases: column_aliases,
18771                with_ordinality: false,
18772                generate_series_args: None,
18773                lateral_subquery: Some(Box::new(inner)),
18774                jsonb_each_text_arg: None,
18775                table_fn_call: None,
18776                rows_from: None,
18777                json_table: None,
18778                scalar_fn_item: false,
18779            });
18780        }
18781        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18782        // function as a FROM item. Emits one row per (key, value)
18783        // pair in the JSONB object argument as TEXT columns. May
18784        // be wrapped in CROSS JOIN LATERAL when the argument
18785        // references a preceding FROM item (sentori migration
18786        // 0067 backfill shape: `CROSS JOIN LATERAL
18787        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18788        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18789            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18790        {
18791            let each_fn = match self.peek() {
18792                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18793                _ => unreachable!(),
18794            };
18795            self.advance(); // jsonb_each[_text] / json_each[_text]
18796            self.advance(); // (
18797            let arg = self.parse_expr(0)?;
18798            if !matches!(self.peek(), Token::RParen) {
18799                return Err(self.err(alloc::format!(
18800                    "expected ')' after {each_fn}() argument, got {:?}",
18801                    self.peek()
18802                )));
18803            }
18804            self.advance();
18805            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18806            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18807            return Ok(TableRef {
18808                name,
18809                alias: alias_ident,
18810                only: false,
18811                as_of_segment: None,
18812                unnest_expr: None,
18813                // `AS t(k, v)` renames key/value positionally, same as the
18814                // LATERAL-position form already does.
18815                unnest_column_aliases: column_aliases,
18816                with_ordinality: false,
18817                generate_series_args: None,
18818                lateral_subquery: None,
18819                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18820                table_fn_call: None,
18821                rows_from: None,
18822                json_table: None,
18823                scalar_fn_item: false,
18824            });
18825        }
18826        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18827        // (+ json_ variants) — record-returning JSON functions with a
18828        // column-definition list. Desugar to a derived table that
18829        // projects each declared column from the JSON via `->>` + a cast,
18830        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18831        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18832            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18833        {
18834            return self.parse_json_to_record_from();
18835        }
18836        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18837        // row is a text[] of capture groups, so it cannot desugar to unnest
18838        // (that would flatten the array). Wrap it as a derived table
18839        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18840        // SRF path already emits one text[] row per match. PG names the column
18841        // `regexp_matches`; an `AS a(col)` alias overrides it.
18842        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18843                if s.eq_ignore_ascii_case("regexp_matches"))
18844            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18845        {
18846            self.advance(); // fn name
18847            self.advance(); // (
18848            let mut fn_args: Vec<Expr> = Vec::new();
18849            loop {
18850                fn_args.push(self.parse_expr(0)?);
18851                if matches!(self.peek(), Token::Comma) {
18852                    self.advance();
18853                    continue;
18854                }
18855                break;
18856            }
18857            if !matches!(self.peek(), Token::RParen) {
18858                return Err(self.err(alloc::format!(
18859                    "expected ')' after regexp_matches() arguments, got {:?}",
18860                    self.peek()
18861                )));
18862            }
18863            self.advance();
18864            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18865            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18866            // it, so it died on the `with` token while every other table function
18867            // accepted it.
18868            let with_ordinality = self.absorb_with_ordinality();
18869            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18870            let table_alias = alias_ident
18871                .clone()
18872                .unwrap_or_else(|| "regexp_matches".to_string());
18873            // PG names a single-column function's output column after the ALIAS
18874            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18875            // `m` reads as that column and not as a whole-row composite. Naming
18876            // it after the function regardless made `SELECT m[1] FROM … AS m`
18877            // subscript a record.
18878            let col_name = column_aliases
18879                .first()
18880                .cloned()
18881                .or_else(|| alias_ident.clone())
18882                .unwrap_or_else(|| "regexp_matches".to_string());
18883            let inner = crate::ast::SelectStatement {
18884                locking: None,
18885                ctes: Vec::new(),
18886                distinct: false,
18887                distinct_on: Vec::new(),
18888                items: alloc::vec![SelectItem::Expr {
18889                    expr: Expr::FunctionCall {
18890                        name: "regexp_matches".to_string(),
18891                        args: fn_args,
18892                    },
18893                    alias: Some(col_name),
18894                }],
18895                from: None,
18896                where_: None,
18897                group_by: None,
18898                group_by_all: false,
18899                having: None,
18900                unions: Vec::new(),
18901                order_by: Vec::new(),
18902                limit: None,
18903                offset: None,
18904                limit_with_ties: false,
18905                window_check_exprs: Vec::new(),
18906            };
18907            return Ok(TableRef {
18908                name: table_alias.clone(),
18909                alias: Some(table_alias),
18910                only: false,
18911                as_of_segment: None,
18912                unnest_expr: None,
18913                unnest_column_aliases: column_aliases,
18914                with_ordinality,
18915                generate_series_args: None,
18916                lateral_subquery: Some(Box::new(inner)),
18917                jsonb_each_text_arg: None,
18918                table_fn_call: None,
18919                rows_from: None,
18920                json_table: None,
18921                // regexp_matches returns text[], a base type: `SELECT m FROM
18922                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18923                scalar_fn_item: true,
18924            });
18925        }
18926        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18927        // / json_ variants as a FROM item. Rewritten into
18928        // `unnest(<same fn>(<expr>))`: the scalar form returns the
18929        // elements as a TEXT array, and the existing unnest SRF path
18930        // materialises one row per element. PG's natural column name
18931        // is `value`; an `AS a(col)` column-alias list overrides it.
18932        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18933                if s.eq_ignore_ascii_case("jsonb_array_elements")
18934                    || s.eq_ignore_ascii_case("json_array_elements")
18935                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18936                    || s.eq_ignore_ascii_case("json_array_elements_text")
18937                    || s.eq_ignore_ascii_case("jsonb_object_keys")
18938                    || s.eq_ignore_ascii_case("json_object_keys")
18939                    || s.eq_ignore_ascii_case("jsonb_path_query")
18940                    || s.eq_ignore_ascii_case("json_path_query")
18941                    || s.eq_ignore_ascii_case("generate_subscripts")
18942                    || s.eq_ignore_ascii_case("string_to_table")
18943                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
18944            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18945        {
18946            let fn_name = match self.peek() {
18947                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18948                _ => unreachable!(),
18949            };
18950            self.advance(); // fn name
18951            self.advance(); // (
18952            let mut fn_args: Vec<Expr> = Vec::new();
18953            loop {
18954                fn_args.push(self.parse_expr(0)?);
18955                if matches!(self.peek(), Token::Comma) {
18956                    self.advance();
18957                    continue;
18958                }
18959                break;
18960            }
18961            if !matches!(self.peek(), Token::RParen) {
18962                return Err(self.err(alloc::format!(
18963                    "expected ')' after {fn_name}() arguments, got {:?}",
18964                    self.peek()
18965                )));
18966            }
18967            self.advance();
18968            let with_ordinality = self.absorb_with_ordinality();
18969            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18970            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18971            // PG's natural column name: the array-elements SRFs
18972            // declare an OUT parameter `value`; jsonb_object_keys
18973            // and generate_subscripts have none, so the column is
18974            // named after the function. A bare table alias on a
18975            // single-column SRF renames the column too (PG: `FROM
18976            // generate_subscripts(a, 1) AS s` projects column s) —
18977            // except for the OUT-parameter SRFs, whose column stays
18978            // `value` under a bare alias.
18979            let natural_col = if fn_name.ends_with("_array_elements")
18980                || fn_name.ends_with("_array_elements_text")
18981            {
18982                "value".to_string()
18983            } else {
18984                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18985            };
18986            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18987            // Keep any further entries — the second names the
18988            // ordinality column under WITH ORDINALITY.
18989            srf_cols.extend(column_aliases.into_iter().skip(1));
18990            // The *_to_table SRFs are row-streams over the existing
18991            // *_to_array scalars — map the call target; the display
18992            // name (alias / column defaults) keeps the SRF spelling.
18993            let call_name = match fn_name.as_str() {
18994                "string_to_table" => "string_to_array".to_string(),
18995                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18996                _ => fn_name,
18997            };
18998            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18999            // preceding FROM item (bare or qualified column) is correlated;
19000            // route it through the per-outer-row lateral channel.
19001            let expr = crate::ast::Expr::FunctionCall {
19002                name: call_name,
19003                args: fn_args,
19004            };
19005            let correlated = Self::expr_has_any_column(&expr);
19006            let tref = TableRef {
19007                name,
19008                alias: alias_ident,
19009                only: false,
19010                as_of_segment: None,
19011                unnest_expr: Some(Box::new(expr)),
19012                unnest_column_aliases: srf_cols,
19013                with_ordinality,
19014                generate_series_args: None,
19015                lateral_subquery: None,
19016                jsonb_each_text_arg: None,
19017                table_fn_call: None,
19018                rows_from: None,
19019                json_table: None,
19020                // Each of these returns a BASE type (jsonb / text / int), so the item's
19021                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19022                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19023                scalar_fn_item: !with_ordinality,
19024            };
19025            return Ok(if correlated {
19026                Self::wrap_correlated_srf(tref)
19027            } else {
19028                tref
19029            });
19030        }
19031        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19032        // explicit parallel-zip syntax. Each entry lowers to its
19033        // array-returning scalar form (unnest(x) → x itself; the
19034        // FROM-SRF rewrite family → their scalar array calls) and
19035        // the list rides the multi-arg unnest zip channel:
19036        // NULL-padded to the longest, WITH ORDINALITY appends the
19037        // counter. generate_series has no scalar array form and
19038        // errors honestly.
19039        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19040            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19041            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19042        {
19043            self.advance(); // ROWS
19044            self.advance(); // FROM
19045            self.advance(); // (
19046            let mut entries: Vec<Expr> = Vec::new();
19047            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19048            // Used only when some entry has no array form.
19049            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19050            loop {
19051                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19052                if !matches!(self.peek(), Token::LParen) {
19053                    return Err(self.err(alloc::format!(
19054                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19055                        self.peek()
19056                    )));
19057                }
19058                self.advance();
19059                let mut fn_args: Vec<Expr> = Vec::new();
19060                if !matches!(self.peek(), Token::RParen) {
19061                    loop {
19062                        fn_args.push(self.parse_expr(0)?);
19063                        if matches!(self.peek(), Token::Comma) {
19064                            self.advance();
19065                            continue;
19066                        }
19067                        break;
19068                    }
19069                }
19070                if !matches!(self.peek(), Token::RParen) {
19071                    return Err(self.err(alloc::format!(
19072                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19073                        self.peek()
19074                    )));
19075                }
19076                self.advance();
19077                let entry = match fn_name.as_str() {
19078                    "unnest" => {
19079                        if fn_args.len() != 1 {
19080                            return Err(
19081                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19082                            );
19083                        }
19084                        fn_args.pop().expect("len checked")
19085                    }
19086                    "jsonb_array_elements"
19087                    | "json_array_elements"
19088                    | "jsonb_array_elements_text"
19089                    | "json_array_elements_text"
19090                    | "jsonb_object_keys"
19091                    | "json_object_keys"
19092                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19093                        name: fn_name,
19094                        args: fn_args,
19095                    },
19096                    "string_to_table" => crate::ast::Expr::FunctionCall {
19097                        name: "string_to_array".to_string(),
19098                        args: fn_args,
19099                    },
19100                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19101                        name: "regexp_split_to_array".to_string(),
19102                        args: fn_args,
19103                    },
19104                    // v7.39 (read01 round 74) — an SRF with no array form
19105                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19106                    // scalar expression to zip, so the WHOLE list switches to the
19107                    // rows_from channel, which runs each function and zips the
19108                    // rows themselves. The all-array case keeps the old lowering:
19109                    // it is well-trodden and this must not disturb it.
19110                    _ => {
19111                        generic.push((fn_name, fn_args));
19112                        if matches!(self.peek(), Token::Comma) {
19113                            self.advance();
19114                            continue;
19115                        }
19116                        break;
19117                    }
19118                };
19119                generic.push((
19120                    // The array-able entries carry their lowered expr along, so a
19121                    // MIXED list still works: the engine sees the scalar array
19122                    // form and unnests it.
19123                    "__array".to_string(),
19124                    alloc::vec![entry.clone()],
19125                ));
19126                entries.push(entry);
19127                if matches!(self.peek(), Token::Comma) {
19128                    self.advance();
19129                    continue;
19130                }
19131                break;
19132            }
19133            if !matches!(self.peek(), Token::RParen) {
19134                return Err(self.err(alloc::format!(
19135                    "expected ')' to close ROWS FROM, got {:?}",
19136                    self.peek()
19137                )));
19138            }
19139            self.advance();
19140            let with_ordinality = self.absorb_with_ordinality();
19141            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19142            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19143            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19144            // list rides the generic channel.
19145            if generic.iter().any(|(n, _)| n != "__array") {
19146                let correlated = generic
19147                    .iter()
19148                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19149                let tref = TableRef {
19150                    name,
19151                    alias: alias_ident,
19152                    only: false,
19153                    as_of_segment: None,
19154                    unnest_expr: None,
19155                    unnest_column_aliases,
19156                    with_ordinality,
19157                    generate_series_args: None,
19158                    lateral_subquery: None,
19159                    jsonb_each_text_arg: None,
19160                    table_fn_call: None,
19161                    rows_from: Some(generic),
19162                    json_table: None,
19163                    scalar_fn_item: false,
19164                };
19165                return Ok(if correlated {
19166                    Self::wrap_correlated_srf(tref)
19167                } else {
19168                    tref
19169                });
19170            }
19171            let correlated = entries.iter().any(Self::expr_has_any_column);
19172            let expr = if entries.len() == 1 {
19173                entries.pop().expect("len checked")
19174            } else {
19175                crate::ast::Expr::FunctionCall {
19176                    name: "__unnest_zip".to_string(),
19177                    args: entries,
19178                }
19179            };
19180            let tref = TableRef {
19181                name,
19182                alias: alias_ident,
19183                only: false,
19184                as_of_segment: None,
19185                unnest_expr: Some(Box::new(expr)),
19186                unnest_column_aliases,
19187                with_ordinality,
19188                generate_series_args: None,
19189                lateral_subquery: None,
19190                jsonb_each_text_arg: None,
19191                table_fn_call: None,
19192                rows_from: None,
19193                json_table: None,
19194                scalar_fn_item: false,
19195            };
19196            return Ok(if correlated {
19197                Self::wrap_correlated_srf(tref)
19198            } else {
19199                tref
19200            });
19201        }
19202        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19203        // source. Detect at the head before the bare-ident fallback;
19204        // unnest is not a reserved token.
19205        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19206            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19207        {
19208            self.advance(); // unnest
19209            self.advance(); // (
19210            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19211            while matches!(self.peek(), Token::Comma) {
19212                self.advance();
19213                srf_args.push(self.parse_expr(0)?);
19214            }
19215            if !matches!(self.peek(), Token::RParen) {
19216                return Err(self.err(alloc::format!(
19217                    "expected ')' after unnest() argument, got {:?}",
19218                    self.peek()
19219                )));
19220            }
19221            self.advance();
19222            // Multi-arg unnest(a, b, …) zips the arrays in
19223            // parallel, NULL-padding to the longest (PG's ROWS
19224            // FROM shorthand). Lower onto the unnest channel as an
19225            // internal marker call the executors unpack.
19226            let expr = if srf_args.len() == 1 {
19227                srf_args.pop().expect("len checked")
19228            } else {
19229                crate::ast::Expr::FunctionCall {
19230                    name: "__unnest_zip".to_string(),
19231                    args: srf_args,
19232                }
19233            };
19234            let with_ordinality = self.absorb_with_ordinality();
19235            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19236            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19237            let correlated = Self::expr_has_any_column(&expr);
19238            let tref = TableRef {
19239                name,
19240                alias: alias_ident,
19241                only: false,
19242                as_of_segment: None,
19243                unnest_expr: Some(Box::new(expr)),
19244                unnest_column_aliases,
19245                with_ordinality,
19246                generate_series_args: None,
19247                lateral_subquery: None,
19248                jsonb_each_text_arg: None,
19249                table_fn_call: None,
19250                rows_from: None,
19251                json_table: None,
19252                scalar_fn_item: false,
19253            };
19254            return Ok(if correlated {
19255                Self::wrap_correlated_srf(tref)
19256            } else {
19257                tref
19258            });
19259        }
19260        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19261        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19262        // generic table-fn arg parser can't read), so it is intercepted
19263        // here BEFORE the generic dispatch. The doc expr may reference
19264        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19265        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19266                if s.eq_ignore_ascii_case("json_table"))
19267            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19268        {
19269            let tref = self.parse_json_table_ref()?;
19270            let correlated = tref
19271                .json_table
19272                .as_deref()
19273                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19274            return Ok(if correlated {
19275                Self::wrap_correlated_srf(tref)
19276            } else {
19277                tref
19278            });
19279        }
19280        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19281        // functions dispatched by name (`pg_partition_tree('t')`,
19282        // `pg_partition_ancestors('t')`). Same head-detection shape as
19283        // unnest; the engine executor owns the row shape per function.
19284        // v7.39 (read01 round 65) — and a USER function in FROM position
19285        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19286        // (generate_series / unnest / the json_each family) keep it — their arms
19287        // sit further down, so they are excluded here by name rather than by
19288        // ordering. Anything else that is an ident followed by `(` is a table
19289        // function; the engine executor decides whether it is a builtin, a
19290        // set-returning user function, or an error.
19291        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19292                if !s.eq_ignore_ascii_case("generate_series")
19293                    && !s.eq_ignore_ascii_case("unnest")
19294                    && !is_json_each_name(s))
19295            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19296        {
19297            // Body out-of-line — this parse sits on the FROM/subquery
19298            // recursion chain (debug frame-cliff discipline).
19299            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19300            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19301            // outer row, so it rides the lateral channel. Same rule the unnest
19302            // arm uses.
19303            let tref = self.parse_table_fn_ref()?;
19304            let correlated = tref
19305                .table_fn_call
19306                .as_deref()
19307                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19308            return Ok(if correlated {
19309                Self::wrap_correlated_srf(tref)
19310            } else {
19311                tref
19312            });
19313        }
19314        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19315        // [, step])` set-returning source. Same shape as unnest:
19316        // detect at the head, parse the comma-separated arg list,
19317        // dispatch downstream through the engine's set-returning
19318        // path. Supports integer triplets (mailrs's `WITH row_no AS
19319        // (SELECT * FROM generate_series(1, N))` pattern) and
19320        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19321        // date-range iteration pattern, which pre-3.10 had no
19322        // direct equivalent in SPG).
19323        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19324            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19325        {
19326            self.advance(); // generate_series
19327            self.advance(); // (
19328            let mut args: Vec<Expr> = Vec::new();
19329            loop {
19330                args.push(self.parse_expr(0)?);
19331                if matches!(self.peek(), Token::Comma) {
19332                    self.advance();
19333                    continue;
19334                }
19335                break;
19336            }
19337            if !matches!(self.peek(), Token::RParen) {
19338                return Err(self.err(alloc::format!(
19339                    "expected ')' after generate_series() arguments, got {:?}",
19340                    self.peek()
19341                )));
19342            }
19343            self.advance();
19344            if args.len() < 2 || args.len() > 3 {
19345                return Err(self.err(alloc::format!(
19346                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19347                    args.len()
19348                )));
19349            }
19350            let with_ordinality = self.absorb_with_ordinality();
19351            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19352            let name = alias_ident
19353                .clone()
19354                .unwrap_or_else(|| "generate_series".to_string());
19355            let correlated = args.iter().any(Self::expr_has_any_column);
19356            let tref = TableRef {
19357                name,
19358                alias: alias_ident,
19359                only: false,
19360                as_of_segment: None,
19361                unnest_expr: None,
19362                unnest_column_aliases: column_aliases,
19363                with_ordinality,
19364                generate_series_args: Some(args),
19365                lateral_subquery: None,
19366                jsonb_each_text_arg: None,
19367                table_fn_call: None,
19368                rows_from: None,
19369                json_table: None,
19370                scalar_fn_item: false,
19371            };
19372            return Ok(if correlated {
19373                Self::wrap_correlated_srf(tref)
19374            } else {
19375                tref
19376            });
19377        }
19378        // v7.16.2 — preserve information_schema / pg_catalog
19379        // qualifiers (mailrs round-10 A.3). The generic
19380        // `expect_ident_like` strip silently drops the schema;
19381        // we want the engine to recognise these PG meta tables
19382        // and synthesise rows from the live catalog. Produce a
19383        // synthetic name (`__spg_info_columns` etc.) so the
19384        // engine's SELECT-side router can dispatch without
19385        // clashing with any user-defined `columns` table.
19386        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19387            (synth, Some(orig))
19388        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19389            (synth, Some(orig))
19390        } else {
19391            (self.expect_ident_like()?, None)
19392        };
19393        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19394        // time-travel clause. Parse BEFORE the alias so the
19395        // alias can still ride at the tail (`tbl AS OF SEGMENT
19396        // '5' alias`). `AS` is a reserved keyword token, while
19397        // `OF` and `SEGMENT` are bare idents.
19398        let as_of_segment = if matches!(self.peek(), Token::As)
19399            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19400        {
19401            self.advance(); // AS
19402            self.advance(); // OF
19403            let kw = match self.peek().clone() {
19404                Token::Ident(s) | Token::QuotedIdent(s) => s,
19405                other => {
19406                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19407                }
19408            };
19409            if !kw.eq_ignore_ascii_case("segment") {
19410                return Err(self.err(format!(
19411                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19412                )));
19413            }
19414            self.advance();
19415            // Segment id literal — accept either a string or
19416            // integer for operator ergonomics.
19417            let id = match self.advance() {
19418                Token::String(s) => s
19419                    .parse::<u32>()
19420                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19421                Token::Integer(n) => u32::try_from(n)
19422                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19423                other => {
19424                    return Err(self.err(format!(
19425                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19426                    )));
19427                }
19428            };
19429            Some(id)
19430        } else {
19431            None
19432        };
19433        // TABLESAMPLE is not a reserved token — keep the bare-ident
19434        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19435        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19436        {
19437            None
19438        } else {
19439            self.parse_optional_alias()?
19440        };
19441        // r1052 — a catalog name rewritten to its synthetic form keeps
19442        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19443        // still binds after `pg_cast` became `__spg_pg_cast`. PG
19444        // semantics: the visible name of `pg_catalog.pg_cast` IS
19445        // `pg_cast`. Without this, every table-name-qualified column
19446        // on a synthesised catalog answered "missing FROM-clause
19447        // entry" — which is the wall pg_dump hit on its first
19448        // pg_proc/pg_cast query.
19449        let alias = match (&alias, &meta_original) {
19450            (None, Some(orig)) if *orig != name => Some(orig.clone()),
19451            _ => alias,
19452        };
19453        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19454        // (PG grammar). BERNOULLI lowers to a per-row
19455        // `random() < p/100` conjunct on the enclosing SELECT's
19456        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19457        // shares the lowering: SPG has no page structure to
19458        // sample, and the row-level form returns the same expected
19459        // fraction. REPEATABLE(seed) promises a deterministic
19460        // sample SPG cannot honour yet — honest error rather than
19461        // a silently ignored seed.
19462        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19463            self.advance();
19464            let method = self.expect_ident_like()?;
19465            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19466                return Err(self.err(alloc::format!(
19467                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19468                )));
19469            }
19470            if !matches!(self.peek(), Token::LParen) {
19471                return Err(self.err(alloc::format!(
19472                    "expected '(' after TABLESAMPLE {}, got {:?}",
19473                    method.to_ascii_uppercase(),
19474                    self.peek()
19475                )));
19476            }
19477            self.advance();
19478            let percent = self.parse_expr(0)?;
19479            if !matches!(self.peek(), Token::RParen) {
19480                return Err(self.err(alloc::format!(
19481                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19482                    self.peek()
19483                )));
19484            }
19485            self.advance();
19486            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19487            // `seed`, so the sample is stable across repeats and rescans.
19488            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19489            let mut sample_seed: Option<Expr> = None;
19490            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19491                self.advance();
19492                if !matches!(self.peek(), Token::LParen) {
19493                    return Err(self.err(alloc::format!(
19494                        "expected '(' after REPEATABLE, got {:?}",
19495                        self.peek()
19496                    )));
19497                }
19498                self.advance();
19499                let seed = self.parse_expr(0)?;
19500                if !matches!(self.peek(), Token::RParen) {
19501                    return Err(self.err(alloc::format!(
19502                        "expected ')' after REPEATABLE seed, got {:?}",
19503                        self.peek()
19504                    )));
19505                }
19506                self.advance();
19507                sample_seed = Some(seed);
19508            }
19509            let draw = match sample_seed {
19510                Some(seed) => Expr::FunctionCall {
19511                    name: "__tsm_fract".to_string(),
19512                    args: alloc::vec![seed],
19513                },
19514                None => Expr::FunctionCall {
19515                    name: "random".to_string(),
19516                    args: Vec::new(),
19517                },
19518            };
19519            self.pending_sample_preds.push(Expr::Binary {
19520                lhs: Box::new(draw),
19521                op: crate::ast::BinOp::Lt,
19522                rhs: Box::new(Expr::Binary {
19523                    lhs: Box::new(percent),
19524                    op: crate::ast::BinOp::Div,
19525                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19526                }),
19527            });
19528        }
19529        Ok(TableRef {
19530            name,
19531            alias,
19532            only,
19533            as_of_segment,
19534            unnest_expr: None,
19535            unnest_column_aliases: Vec::new(),
19536            with_ordinality: false,
19537            generate_series_args: None,
19538            lateral_subquery: None,
19539            jsonb_each_text_arg: None,
19540            table_fn_call: None,
19541            rows_from: None,
19542            json_table: None,
19543            scalar_fn_item: false,
19544        })
19545    }
19546
19547    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19548    /// but also accepts `AS alias(col [, col, …])` — the
19549    /// PG-standard table-function column-list form. The column
19550    /// list is only honoured when paired with `UNNEST(...)` in
19551    /// the parent; other call sites currently discard it.
19552    /// True when the expression tree contains a qualified column
19553    /// reference (`t.col`) — the syntactic marker that an SRF
19554    /// argument correlates with a preceding FROM item.
19555    fn expr_has_qualified_column(e: &Expr) -> bool {
19556        match e {
19557            Expr::Column(c) => c.qualifier.is_some(),
19558            Expr::Binary { lhs, rhs, .. } => {
19559                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19560            }
19561            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19562            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19563            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19564            Expr::Case {
19565                operand,
19566                branches,
19567                else_branch,
19568            } => {
19569                operand
19570                    .as_deref()
19571                    .is_some_and(Self::expr_has_qualified_column)
19572                    || branches.iter().any(|(w, t)| {
19573                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19574                    })
19575                    || else_branch
19576                        .as_deref()
19577                        .is_some_and(Self::expr_has_qualified_column)
19578            }
19579            _ => false,
19580        }
19581    }
19582
19583    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19584    /// counts a bare (unqualified) column. A set-returning function has no
19585    /// input columns of its own, so ANY column in its arguments is an outer
19586    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19587    fn expr_has_any_column(e: &Expr) -> bool {
19588        match e {
19589            Expr::Column(_) => true,
19590            Expr::Binary { lhs, rhs, .. } => {
19591                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19592            }
19593            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19594            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19595            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19596            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19597            // constructor or subscript fell to the `_ => false` arm, so
19598            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19599            // channel and the eager peer eval answered `column "x" does
19600            // not exist` (the substitution walker already recurses both
19601            // shapes; only this detector was blind to them).
19602            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19603            Expr::ArraySubscript { target, index } => {
19604                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19605            }
19606            Expr::Case {
19607                operand,
19608                branches,
19609                else_branch,
19610            } => {
19611                operand.as_deref().is_some_and(Self::expr_has_any_column)
19612                    || branches
19613                        .iter()
19614                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19615                    || else_branch
19616                        .as_deref()
19617                        .is_some_and(Self::expr_has_any_column)
19618            }
19619            _ => false,
19620        }
19621    }
19622
19623    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19624    /// `generate_series(1, t.n)`) into the lateral_subquery
19625    /// channel: `SELECT * FROM <srf>` executes per outer row with
19626    /// outer references substituted (v7.37.43-T4.5 machinery).
19627    /// Uncorrelated SRFs stay on their plain channels.
19628    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19629        let name = srf.name.clone();
19630        let alias = srf.alias.clone();
19631        let inner = crate::ast::SelectStatement {
19632            locking: None,
19633            ctes: Vec::new(),
19634            distinct: false,
19635            distinct_on: Vec::new(),
19636            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19637            from: Some(crate::ast::FromClause {
19638                primary: srf,
19639                joins: Vec::new(),
19640            }),
19641            where_: None,
19642            group_by: None,
19643            group_by_all: false,
19644            having: None,
19645            unions: Vec::new(),
19646            order_by: Vec::new(),
19647            limit: None,
19648            offset: None,
19649            limit_with_ties: false,
19650            window_check_exprs: Vec::new(),
19651        };
19652        TableRef {
19653            name,
19654            alias,
19655            only: false,
19656            as_of_segment: None,
19657            unnest_expr: None,
19658            unnest_column_aliases: Vec::new(),
19659            with_ordinality: false,
19660            generate_series_args: None,
19661            lateral_subquery: Some(Box::new(inner)),
19662            jsonb_each_text_arg: None,
19663            table_fn_call: None,
19664            rows_from: None,
19665            json_table: None,
19666            scalar_fn_item: false,
19667        }
19668    }
19669
19670    /// True when the expression tree contains an unresolved
19671    /// `OVER w` marker (see parse_over_clause).
19672    fn expr_has_named_window(e: &Expr) -> bool {
19673        match e {
19674            Expr::WindowFunction { partition_by, .. } => matches!(
19675                partition_by.as_slice(),
19676                [Expr::Column(c)] if matches!(
19677                    c.qualifier.as_deref(),
19678                    Some("__named_window__") | Some("__named_window_ref__")
19679                )
19680            ),
19681            Expr::Binary { lhs, rhs, .. } => {
19682                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19683            }
19684            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19685            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19686            Expr::Case {
19687                operand,
19688                branches,
19689                else_branch,
19690            } => {
19691                operand.as_deref().is_some_and(Self::expr_has_named_window)
19692                    || branches.iter().any(|(w, t)| {
19693                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19694                    })
19695                    || else_branch
19696                        .as_deref()
19697                        .is_some_and(Self::expr_has_named_window)
19698            }
19699            _ => false,
19700        }
19701    }
19702
19703    /// v7.39 (round 705) — the NAMES the expression references through the
19704    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19705    /// definitions nothing referenced. Traversal mirrors
19706    /// `expr_has_named_window` above.
19707    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19708        match e {
19709            Expr::WindowFunction { partition_by, .. } => {
19710                if let [Expr::Column(c)] = partition_by.as_slice()
19711                    && matches!(
19712                        c.qualifier.as_deref(),
19713                        Some("__named_window__") | Some("__named_window_ref__")
19714                    )
19715                {
19716                    into.push(c.name.clone());
19717                }
19718            }
19719            Expr::Binary { lhs, rhs, .. } => {
19720                Self::collect_named_window_refs(lhs, into);
19721                Self::collect_named_window_refs(rhs, into);
19722            }
19723            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19724                Self::collect_named_window_refs(expr, into);
19725            }
19726            Expr::FunctionCall { args, .. } => {
19727                for a in args {
19728                    Self::collect_named_window_refs(a, into);
19729                }
19730            }
19731            Expr::Case {
19732                operand,
19733                branches,
19734                else_branch,
19735            } => {
19736                if let Some(o) = operand.as_deref() {
19737                    Self::collect_named_window_refs(o, into);
19738                }
19739                for (w, t) in branches {
19740                    Self::collect_named_window_refs(w, into);
19741                    Self::collect_named_window_refs(t, into);
19742                }
19743                if let Some(eb) = else_branch.as_deref() {
19744                    Self::collect_named_window_refs(eb, into);
19745                }
19746            }
19747            _ => {}
19748        }
19749    }
19750
19751    /// Inline named-window definitions into the `OVER w` markers.
19752    /// An unknown name errors (PG: window "w" does not exist).
19753    #[allow(clippy::type_complexity)]
19754    fn substitute_named_windows(
19755        e: &mut Expr,
19756        defs: &[(
19757            String,
19758            (
19759                Vec<Expr>,
19760                Vec<(Expr, bool, Option<bool>)>,
19761                Option<WindowFrame>,
19762            ),
19763        )],
19764    ) -> Result<(), String> {
19765        match e {
19766            Expr::WindowFunction {
19767                partition_by,
19768                order_by,
19769                frame,
19770                ..
19771            } => {
19772                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19773                // from the bare `OVER w1` (a plain reference).
19774                let named = match partition_by.as_slice() {
19775                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19776                        Some("__named_window__") => Some((c.name.clone(), false)),
19777                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19778                        _ => None,
19779                    },
19780                    _ => None,
19781                };
19782                if let Some((wname, is_copy)) = named {
19783                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19784                    else {
19785                        return Err(alloc::format!("window {wname:?} does not exist"));
19786                    };
19787                    if !is_copy {
19788                        *partition_by = def.0.clone();
19789                        *order_by = def.1.clone();
19790                        *frame = def.2.clone();
19791                        return Ok(());
19792                    }
19793                    // v7.39 (round 229) — PG's copy rules, probed against
19794                    // 18.4: a copy inherits the partitioning, may supply an
19795                    // ordering only when the base has none, and may not copy
19796                    // a base that already carries a frame (its own frame
19797                    // would be ambiguous with the inherited one).
19798                    if !def.1.is_empty() && !order_by.is_empty() {
19799                        return Err(alloc::format!(
19800                            "cannot override ORDER BY clause of window \"{wname}\""
19801                        ));
19802                    }
19803                    if def.2.is_some() {
19804                        return Err(alloc::format!(
19805                            "cannot copy window \"{wname}\" because it has a frame clause"
19806                        ));
19807                    }
19808                    *partition_by = def.0.clone();
19809                    if order_by.is_empty() {
19810                        *order_by = def.1.clone();
19811                    }
19812                }
19813                Ok(())
19814            }
19815            Expr::Binary { lhs, rhs, .. } => {
19816                Self::substitute_named_windows(lhs, defs)?;
19817                Self::substitute_named_windows(rhs, defs)
19818            }
19819            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19820                Self::substitute_named_windows(expr, defs)
19821            }
19822            Expr::FunctionCall { args, .. } => {
19823                for a in args {
19824                    Self::substitute_named_windows(a, defs)?;
19825                }
19826                Ok(())
19827            }
19828            Expr::Case {
19829                operand,
19830                branches,
19831                else_branch,
19832            } => {
19833                if let Some(op) = operand {
19834                    Self::substitute_named_windows(op, defs)?;
19835                }
19836                for (w, t) in branches {
19837                    Self::substitute_named_windows(w, defs)?;
19838                    Self::substitute_named_windows(t, defs)?;
19839                }
19840                if let Some(el) = else_branch {
19841                    Self::substitute_named_windows(el, defs)?;
19842                }
19843                Ok(())
19844            }
19845            _ => Ok(()),
19846        }
19847    }
19848
19849    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19850    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19851    /// composition.
19852    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19853        debug_assert!(matches!(self.peek(), Token::Table));
19854        self.advance(); // TABLE
19855        let tname = self.expect_ident_like()?;
19856        Ok(SelectStatement {
19857            locking: None,
19858            ctes: Vec::new(),
19859            distinct: false,
19860            distinct_on: Vec::new(),
19861            items: alloc::vec![SelectItem::Wildcard],
19862            from: Some(FromClause {
19863                primary: TableRef {
19864                    name: tname,
19865                    alias: None,
19866                    only: false,
19867                    as_of_segment: None,
19868                    unnest_expr: None,
19869                    unnest_column_aliases: Vec::new(),
19870                    with_ordinality: false,
19871                    generate_series_args: None,
19872                    lateral_subquery: None,
19873                    jsonb_each_text_arg: None,
19874                    table_fn_call: None,
19875                    rows_from: None,
19876                    json_table: None,
19877                    scalar_fn_item: false,
19878                },
19879                joins: Vec::new(),
19880            }),
19881            where_: None,
19882            group_by: None,
19883            group_by_all: false,
19884            having: None,
19885            unions: Vec::new(),
19886            order_by: Vec::new(),
19887            limit: None,
19888            offset: None,
19889            limit_with_ties: false,
19890            window_check_exprs: Vec::new(),
19891        })
19892    }
19893
19894    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19895    /// variants) → a derived table that reads each declared column out of
19896    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19897    /// `jsonb_array_elements(J)` (one row per element, column `value`);
19898    /// the scalar *record form projects a single row straight off `J`.
19899    /// Rides the existing lateral-subquery channel, so no new executor or
19900    /// AST is needed.
19901    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19902        use crate::ast::{
19903            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19904        };
19905        let fn_name = match self.peek() {
19906            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19907            _ => unreachable!("caller guarded is_json_to_record_name"),
19908        };
19909        self.advance(); // fn name
19910        self.advance(); // (
19911        let mut arg = self.parse_expr(0)?;
19912        // populate_record(base, json): the base only carries the record
19913        // type here — the JSON argument is the second expression.
19914        let mut base: Option<Expr> = None;
19915        if matches!(self.peek(), Token::Comma) {
19916            self.advance();
19917            base = Some(arg);
19918            arg = self.parse_expr(0)?;
19919        }
19920        if !matches!(self.peek(), Token::RParen) {
19921            return Err(self.err(alloc::format!(
19922                "expected ')' after {fn_name}() argument, got {:?}",
19923                self.peek()
19924            )));
19925        }
19926        self.advance(); // )
19927        let is_set = fn_name.ends_with("recordset");
19928        // `[AS] alias ( col type [, …] )` column-definition list.
19929        if matches!(self.peek(), Token::As) {
19930            self.advance();
19931        }
19932        let alias_opt = match self.peek() {
19933            Token::Ident(s) | Token::QuotedIdent(s) => {
19934                let a = s.clone();
19935                self.advance();
19936                Some(a)
19937            }
19938            _ => None,
19939        };
19940        // v7.39 (read01 round 76) — the populate family's canonical PG
19941        // spelling carries no column list at all: the row shape comes from
19942        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19943        // j)`). The parser has no catalog, so hand the two arguments to the
19944        // engine's table-function channel, which does. Only `*_to_record*`
19945        // (whose base is bare `record`) genuinely requires the list.
19946        if !matches!(self.peek(), Token::LParen) {
19947            if let Some(base_expr) = base {
19948                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19949                return Ok(TableRef {
19950                    name: alias.clone(),
19951                    alias: Some(alias),
19952                    only: false,
19953                    as_of_segment: None,
19954                    unnest_expr: None,
19955                    unnest_column_aliases: Vec::new(),
19956                    with_ordinality: false,
19957                    generate_series_args: None,
19958                    lateral_subquery: None,
19959                    jsonb_each_text_arg: None,
19960                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19961                    rows_from: None,
19962                    json_table: None,
19963                    scalar_fn_item: false,
19964                });
19965            }
19966            return Err(self.err(alloc::format!(
19967                "expected '(' to start the {fn_name} column-definition list, got {:?}",
19968                self.peek()
19969            )));
19970        }
19971        let Some(alias) = alias_opt else {
19972            return Err(self.err(alloc::format!(
19973                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19974            )));
19975        };
19976        self.advance(); // (
19977        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19978        loop {
19979            let col = self.expect_ident_like()?;
19980            let ty = self.parse_cast_target()?;
19981            coldefs.push((col, ty));
19982            if matches!(self.peek(), Token::Comma) {
19983                self.advance();
19984                continue;
19985            }
19986            if matches!(self.peek(), Token::RParen) {
19987                self.advance();
19988                break;
19989            }
19990            return Err(self.err(alloc::format!(
19991                "expected ',' or ')' in {fn_name} column list, got {:?}",
19992                self.peek()
19993            )));
19994        }
19995        if coldefs.is_empty() {
19996            return Err(self.err(alloc::format!(
19997                "{fn_name} column-definition list must declare at least one column"
19998            )));
19999        }
20000        // Per column: (base ->> 'col')::type AS col. The base is the
20001        // per-element `value` column for the *set form, or the argument
20002        // itself for the scalar record form.
20003        let items: Vec<SelectItem> = coldefs
20004            .into_iter()
20005            .map(|(col, ty)| {
20006                let base = if is_set {
20007                    Expr::Column(ColumnName {
20008                        qualifier: None,
20009                        name: "value".to_string(),
20010                    })
20011                } else {
20012                    arg.clone()
20013                };
20014                SelectItem::Expr {
20015                    expr: Expr::Cast {
20016                        expr: Box::new(Expr::Binary {
20017                            lhs: Box::new(base),
20018                            op: BinOp::JsonGetText,
20019                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20020                        }),
20021                        target: ty,
20022                    },
20023                    alias: Some(col),
20024                }
20025            })
20026            .collect();
20027        let from = if is_set {
20028            let elem_fn = if fn_name.starts_with("jsonb") {
20029                "jsonb_array_elements"
20030            } else {
20031                "json_array_elements"
20032            };
20033            Some(FromClause {
20034                primary: TableRef {
20035                    name: "value".to_string(),
20036                    alias: None,
20037                    only: false,
20038                    as_of_segment: None,
20039                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20040                        name: elem_fn.to_string(),
20041                        args: alloc::vec![arg],
20042                    })),
20043                    unnest_column_aliases: alloc::vec!["value".to_string()],
20044                    with_ordinality: false,
20045                    generate_series_args: None,
20046                    lateral_subquery: None,
20047                    jsonb_each_text_arg: None,
20048                    table_fn_call: None,
20049                    rows_from: None,
20050                    json_table: None,
20051                    scalar_fn_item: false,
20052                },
20053                joins: Vec::new(),
20054            })
20055        } else {
20056            None
20057        };
20058        let inner = SelectStatement {
20059            locking: None,
20060            ctes: Vec::new(),
20061            distinct: false,
20062            distinct_on: Vec::new(),
20063            items,
20064            from,
20065            where_: None,
20066            group_by: None,
20067            group_by_all: false,
20068            having: None,
20069            unions: Vec::new(),
20070            order_by: Vec::new(),
20071            limit: None,
20072            offset: None,
20073            limit_with_ties: false,
20074            window_check_exprs: Vec::new(),
20075        };
20076        Ok(TableRef {
20077            name: alias.clone(),
20078            alias: Some(alias),
20079            only: false,
20080            as_of_segment: None,
20081            unnest_expr: None,
20082            unnest_column_aliases: Vec::new(),
20083            with_ordinality: false,
20084            generate_series_args: None,
20085            lateral_subquery: Some(Box::new(inner)),
20086            jsonb_each_text_arg: None,
20087            table_fn_call: None,
20088            rows_from: None,
20089            json_table: None,
20090            scalar_fn_item: false,
20091        })
20092    }
20093
20094    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20095    /// Returns true when the clause was present. `WITH` alone (a
20096    /// CTE can never start here) is not enough — the ORDINALITY
20097    /// ident must follow, so a stray WITH still errors downstream.
20098    fn absorb_with_ordinality(&mut self) -> bool {
20099        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20100            && matches!(self.tokens.get(self.pos + 1),
20101                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20102        {
20103            self.advance();
20104            self.advance();
20105            true
20106        } else {
20107            false
20108        }
20109    }
20110
20111    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20112    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20113    /// Out-of-line: the caller sits on the FROM recursion chain.
20114    #[inline(never)]
20115    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20116        let fn_name = match self.advance() {
20117            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20118            _ => unreachable!("caller peeked an ident"),
20119        };
20120        self.advance(); // (
20121        let mut args: Vec<Expr> = Vec::new();
20122        if !matches!(self.peek(), Token::RParen) {
20123            loop {
20124                args.push(self.parse_expr(0)?);
20125                if matches!(self.peek(), Token::Comma) {
20126                    self.advance();
20127                    continue;
20128                }
20129                break;
20130            }
20131        }
20132        if !matches!(self.peek(), Token::RParen) {
20133            return Err(self.err(alloc::format!(
20134                "expected ')' after {fn_name}() arguments, got {:?}",
20135                self.peek()
20136            )));
20137        }
20138        self.advance();
20139        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20140        // counter column rides after the function's own, and the alias list
20141        // names it.
20142        let with_ordinality = self.absorb_with_ordinality();
20143        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20144        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20145        Ok(TableRef {
20146            name,
20147            alias: alias_ident,
20148            only: false,
20149            as_of_segment: None,
20150            unnest_expr: None,
20151            unnest_column_aliases,
20152            with_ordinality,
20153            generate_series_args: None,
20154            lateral_subquery: None,
20155            jsonb_each_text_arg: None,
20156            table_fn_call: Some(Box::new((fn_name, args))),
20157            rows_from: None,
20158            json_table: None,
20159            scalar_fn_item: false,
20160        })
20161    }
20162
20163    /// v7.39 (round 205, JSON_TABLE) — parse
20164    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20165    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20166    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20167    #[inline(never)]
20168    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20169        self.advance(); // json_table
20170        self.advance(); // (
20171        let doc = Box::new(self.parse_expr(0)?);
20172        self.expect_comma_json_table()?;
20173        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20174        // Optional `PASSING <expr> AS <name> [, …]`.
20175        let mut passing: Vec<(String, Expr)> = Vec::new();
20176        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20177            self.advance();
20178            loop {
20179                let e = self.parse_expr(0)?;
20180                if !matches!(self.peek(), Token::As) {
20181                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20182                }
20183                self.advance();
20184                let vname = match self.advance() {
20185                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20186                    other => {
20187                        return Err(self.err(alloc::format!(
20188                            "expected PASSING variable name, got {other:?}"
20189                        )));
20190                    }
20191                };
20192                passing.push((vname, e));
20193                if matches!(self.peek(), Token::Comma) {
20194                    self.advance();
20195                    continue;
20196                }
20197                break;
20198            }
20199        }
20200        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20201            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20202        }
20203        self.advance();
20204        let columns = self.parse_json_table_columns()?;
20205        if !matches!(self.peek(), Token::RParen) {
20206            return Err(self.err(alloc::format!(
20207                "expected ')' to close JSON_TABLE, got {:?}",
20208                self.peek()
20209            )));
20210        }
20211        self.advance();
20212        let alias_ident = self.parse_optional_alias()?;
20213        let name = alias_ident
20214            .clone()
20215            .unwrap_or_else(|| String::from("json_table"));
20216        Ok(TableRef {
20217            name,
20218            alias: alias_ident,
20219            only: false,
20220            as_of_segment: None,
20221            unnest_expr: None,
20222            unnest_column_aliases: Vec::new(),
20223            with_ordinality: false,
20224            generate_series_args: None,
20225            lateral_subquery: None,
20226            jsonb_each_text_arg: None,
20227            table_fn_call: None,
20228            rows_from: None,
20229            json_table: Some(Box::new(crate::ast::JsonTable {
20230                doc,
20231                row_path,
20232                columns,
20233                passing,
20234            })),
20235            scalar_fn_item: false,
20236        })
20237    }
20238
20239    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20240        if !matches!(self.peek(), Token::Comma) {
20241            return Err(self.err(alloc::format!(
20242                "expected ',' after JSON_TABLE document, got {:?}",
20243                self.peek()
20244            )));
20245        }
20246        self.advance();
20247        Ok(())
20248    }
20249
20250    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20251        match self.advance() {
20252            Token::String(s) => Ok(s),
20253            other => Err(self.err(alloc::format!(
20254                "expected {what} string literal, got {other:?}"
20255            ))),
20256        }
20257    }
20258
20259    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20260    #[inline(never)]
20261    fn parse_json_table_columns(
20262        &mut self,
20263    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20264        if !matches!(self.peek(), Token::LParen) {
20265            return Err(self.err("expected '(' after COLUMNS".into()));
20266        }
20267        self.advance();
20268        let mut cols = Vec::new();
20269        loop {
20270            cols.push(self.parse_json_table_one_column()?);
20271            if matches!(self.peek(), Token::Comma) {
20272                self.advance();
20273                continue;
20274            }
20275            break;
20276        }
20277        if !matches!(self.peek(), Token::RParen) {
20278            return Err(self.err(alloc::format!(
20279                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20280                self.peek()
20281            )));
20282        }
20283        self.advance();
20284        Ok(cols)
20285    }
20286
20287    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20288        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20289        // NESTED [PATH] '<p>' COLUMNS (...)
20290        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20291            self.advance();
20292            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20293                self.advance();
20294            }
20295            let path = self.parse_json_string_literal("NESTED PATH")?;
20296            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20297                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20298            }
20299            self.advance();
20300            let columns = self.parse_json_table_columns()?;
20301            return Ok(JsonTableColumn::Nested { path, columns });
20302        }
20303        // <name> ...
20304        let name = match self.advance() {
20305            Token::Ident(s) | Token::QuotedIdent(s) => s,
20306            other => {
20307                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20308            }
20309        };
20310        // <name> FOR ORDINALITY
20311        if matches!(self.peek(), Token::For) {
20312            self.advance();
20313            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20314                return Err(self.err("expected ORDINALITY after FOR".into()));
20315            }
20316            self.advance();
20317            return Ok(JsonTableColumn::Ordinality { name });
20318        }
20319        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20320        let ty = self.parse_column_type_name()?;
20321        let mut format_json = false;
20322        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20323            self.advance();
20324            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20325                return Err(self.err("expected JSON after FORMAT".into()));
20326            }
20327            self.advance();
20328            format_json = true;
20329        }
20330        let mut exists = false;
20331        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20332            self.advance();
20333            exists = true;
20334        }
20335        let mut path = alloc::format!("$.{name}");
20336        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20337            self.advance();
20338            path = self.parse_json_string_literal("column PATH")?;
20339        }
20340        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20341            // `FORMAT JSON` after PATH (alternate placement).
20342            self.advance();
20343            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20344                self.advance();
20345            }
20346            format_json = true;
20347        }
20348        let mut wrapper = false;
20349        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20350            self.advance();
20351            // optional CONDITIONAL/UNCONDITIONAL
20352            if matches!(self.peek(), Token::Ident(s)
20353                if s.eq_ignore_ascii_case("unconditional")
20354                    || s.eq_ignore_ascii_case("conditional"))
20355            {
20356                self.advance();
20357            }
20358            if !matches!(self.peek(), Token::Ident(s)
20359                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20360            {
20361                return Err(self.err("expected WRAPPER after WITH".into()));
20362            }
20363            self.advance();
20364            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20365            if matches!(self.peek(), Token::Ident(s)
20366                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20367            {
20368                self.advance();
20369            }
20370            wrapper = true;
20371        }
20372        // ON EMPTY / ON ERROR clauses (two, in any order).
20373        let mut on_empty = JsonTableOnBehavior::Null;
20374        let mut on_error = JsonTableOnBehavior::Null;
20375        for _ in 0..2 {
20376            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20377            {
20378                self.advance();
20379                Some(JsonTableOnBehavior::Error)
20380            } else if matches!(self.peek(), Token::Null) {
20381                self.advance();
20382                Some(JsonTableOnBehavior::Null)
20383            } else if matches!(self.peek(), Token::Default) {
20384                self.advance();
20385                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20386            } else {
20387                None
20388            };
20389            let Some(behavior) = behavior else { break };
20390            // `ON {EMPTY|ERROR}`
20391            if !matches!(self.peek(), Token::On) {
20392                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20393            }
20394            self.advance();
20395            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20396                self.advance();
20397                on_empty = behavior;
20398            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20399                self.advance();
20400                on_error = behavior;
20401            } else {
20402                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20403            }
20404        }
20405        Ok(JsonTableColumn::Regular {
20406            name,
20407            ty,
20408            path,
20409            exists,
20410            format_json,
20411            wrapper,
20412            on_empty,
20413            on_error,
20414        })
20415    }
20416
20417    fn parse_optional_alias_with_columns(
20418        &mut self,
20419    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20420        let alias = self.parse_optional_alias()?;
20421        if alias.is_none() {
20422            return Ok((None, Vec::new()));
20423        }
20424        let mut cols: Vec<String> = Vec::new();
20425        if matches!(self.peek(), Token::LParen) {
20426            self.advance();
20427            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20428                self.advance();
20429                cols.push(s);
20430                if matches!(self.peek(), Token::Comma) {
20431                    self.advance();
20432                    continue;
20433                }
20434                break;
20435            }
20436            if matches!(self.peek(), Token::RParen) {
20437                self.advance();
20438            }
20439        }
20440        Ok((alias, cols))
20441    }
20442
20443    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20444    /// whose keyword token was already consumed and whose `(` is the
20445    /// current token. Factored out of `parse_atom` (and marked
20446    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20447    /// recursive `parse_atom` frame — inlining them there enlarges the
20448    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20449    /// against, risking an overflow before the budget triggers.
20450    #[inline(never)]
20451    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20452        self.advance(); // (
20453        let mut args = Vec::new();
20454        if !matches!(self.peek(), Token::RParen) {
20455            loop {
20456                args.push(self.parse_expr(0)?);
20457                match self.peek() {
20458                    Token::Comma => {
20459                        self.advance();
20460                    }
20461                    Token::RParen => break,
20462                    other => {
20463                        return Err(self.err(alloc::format!(
20464                            "expected ',' or ')' in {name}() args, got {other:?}"
20465                        )));
20466                    }
20467                }
20468            }
20469        }
20470        self.advance(); // )
20471        Ok(Expr::FunctionCall {
20472            name: name.into(),
20473            args,
20474        })
20475    }
20476
20477    /// FROM-clause: a primary table reference plus zero-or-more joined
20478    /// peers expressed via either `, <table>` (cross-product, no ON) or
20479    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20480    /// v1.10 keeps the join list flat (left-associative nested-loop
20481    /// semantics).
20482    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20483        let primary = self.parse_table_ref()?;
20484        let primary_qual = primary
20485            .alias
20486            .clone()
20487            .unwrap_or_else(|| primary.name.clone());
20488        let joins = self.parse_from_joins(&primary_qual)?;
20489        Ok(FromClause { primary, joins })
20490    }
20491
20492    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20493    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20494    /// SAME grammar after its target table has already been consumed.
20495    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20496    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20497    /// be parsed forward, once.)
20498    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20499    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20500    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20501    /// desugaring, which needs a name for the left side of each equality.
20502    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20503        let mut joins = Vec::new();
20504        loop {
20505            // `, <table>` — cross-product with no ON.
20506            if matches!(self.peek(), Token::Comma) {
20507                self.advance();
20508                let table = self.parse_table_ref()?;
20509                joins.push(FromJoin {
20510                    kind: JoinKind::Cross,
20511                    table,
20512                    on: None,
20513                    using_cols: None,
20514                    natural: false,
20515                });
20516                continue;
20517            }
20518            // v7.37.16 — optional leading `NATURAL` before the join
20519            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20520            // not a lexer keyword (it arrives as a bare Ident), so match
20521            // it case-insensitively here. When present, no ON/USING
20522            // clause is allowed — the common columns are resolved at
20523            // execution time.
20524            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20525            if natural {
20526                self.advance();
20527            }
20528            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20529            // CROSS JOIN, and bare JOIN (defaults to INNER).
20530            let kind =
20531                match self.peek() {
20532                    Token::Inner => {
20533                        self.advance();
20534                        if !matches!(self.peek(), Token::Join) {
20535                            return Err(self
20536                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20537                        }
20538                        self.advance();
20539                        JoinKind::Inner
20540                    }
20541                    Token::Left => {
20542                        self.advance();
20543                        if matches!(self.peek(), Token::Outer) {
20544                            self.advance();
20545                        }
20546                        if !matches!(self.peek(), Token::Join) {
20547                            return Err(self.err(format!(
20548                                "expected JOIN after LEFT [OUTER], got {:?}",
20549                                self.peek()
20550                            )));
20551                        }
20552                        self.advance();
20553                        JoinKind::Left
20554                    }
20555                    Token::Cross => {
20556                        self.advance();
20557                        if !matches!(self.peek(), Token::Join) {
20558                            return Err(self
20559                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20560                        }
20561                        self.advance();
20562                        JoinKind::Cross
20563                    }
20564                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20565                    Token::Right => {
20566                        self.advance();
20567                        if matches!(self.peek(), Token::Outer) {
20568                            self.advance();
20569                        }
20570                        if !matches!(self.peek(), Token::Join) {
20571                            return Err(self.err(format!(
20572                                "expected JOIN after RIGHT [OUTER], got {:?}",
20573                                self.peek()
20574                            )));
20575                        }
20576                        self.advance();
20577                        JoinKind::Right
20578                    }
20579                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20580                    Token::Full => {
20581                        self.advance();
20582                        if matches!(self.peek(), Token::Outer) {
20583                            self.advance();
20584                        }
20585                        if !matches!(self.peek(), Token::Join) {
20586                            return Err(self.err(format!(
20587                                "expected JOIN after FULL [OUTER], got {:?}",
20588                                self.peek()
20589                            )));
20590                        }
20591                        self.advance();
20592                        JoinKind::FullOuter
20593                    }
20594                    Token::Join => {
20595                        self.advance();
20596                        JoinKind::Inner
20597                    }
20598                    _ => break,
20599                };
20600            let table = self.parse_table_ref()?;
20601            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20602            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20603            // where prev_table is the most-recent left-side table
20604            // (the previous join's table if any, else the FROM primary).
20605            // PG semantics around column merging are richer (USING'd
20606            // cols become deduplicated single output columns); for
20607            // sugar purposes the predicate-only form covers the
20608            // baseline corpus shape and chained `… JOIN x USING (k)
20609            // JOIN y USING (k)` calls.
20610            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20611            // common columns resolve at execution time.
20612            if natural {
20613                joins.push(FromJoin {
20614                    kind,
20615                    table,
20616                    on: None,
20617                    using_cols: None,
20618                    natural: true,
20619                });
20620                continue;
20621            }
20622            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20623            // v7.37.16 — capture the USING column list (in addition to
20624            // the ON desugar below) so the executor can perform PG's
20625            // column-merge on the output side.
20626            let mut using_cols: Option<Vec<String>> = None;
20627            let on = if matches!(self.peek(), Token::On) {
20628                self.advance();
20629                Some(self.parse_expr(0)?)
20630            } else if using_match {
20631                self.advance();
20632                if !matches!(self.peek(), Token::LParen) {
20633                    return Err(
20634                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20635                    );
20636                }
20637                self.advance();
20638                let mut cols: Vec<String> = Vec::new();
20639                loop {
20640                    match self.peek().clone() {
20641                        Token::Ident(s) | Token::QuotedIdent(s) => {
20642                            self.advance();
20643                            cols.push(s);
20644                        }
20645                        other => {
20646                            return Err(self.err(format!(
20647                                "expected column name inside USING (…), got {other:?}"
20648                            )));
20649                        }
20650                    }
20651                    match self.peek() {
20652                        Token::Comma => {
20653                            self.advance();
20654                            continue;
20655                        }
20656                        Token::RParen => {
20657                            self.advance();
20658                            break;
20659                        }
20660                        other => {
20661                            return Err(self.err(format!(
20662                                "expected ',' or ')' inside USING (…), got {other:?}"
20663                            )));
20664                        }
20665                    }
20666                }
20667                if cols.is_empty() {
20668                    return Err(self.err("USING (…) requires at least one column".to_string()));
20669                }
20670                using_cols = Some(cols.clone());
20671                // Pick the left-side alias: prev join's table if any,
20672                // else FROM primary. Use alias when present, else
20673                // table name (PG-equivalent qualifier).
20674                let left_qual: String = joins
20675                    .last()
20676                    .map(|j| {
20677                        j.table
20678                            .alias
20679                            .clone()
20680                            .unwrap_or_else(|| j.table.name.clone())
20681                    })
20682                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20683                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20684                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20685                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20686                        qualifier: Some(left_qual.clone()),
20687                        name: c.clone(),
20688                    })),
20689                    op: crate::ast::BinOp::Eq,
20690                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20691                        qualifier: Some(right_qual.clone()),
20692                        name: c,
20693                    })),
20694                });
20695                let first = iter.next().expect("at least one col");
20696                Some(iter.fold(first, |acc, pred| Expr::Binary {
20697                    lhs: alloc::boxed::Box::new(acc),
20698                    op: crate::ast::BinOp::And,
20699                    rhs: alloc::boxed::Box::new(pred),
20700                }))
20701            } else if kind == JoinKind::Cross {
20702                None
20703            } else {
20704                return Err(self.err(format!(
20705                    "expected ON or USING after {:?} JOIN, got {:?}",
20706                    kind,
20707                    self.peek()
20708                )));
20709            };
20710            joins.push(FromJoin {
20711                kind,
20712                table,
20713                on,
20714                using_cols,
20715                natural: false,
20716            });
20717        }
20718        Ok(joins)
20719    }
20720
20721    /// Optional alias after an expression or table:
20722    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20723    /// accepted (PG-style implicit alias). Returns `None` if the next token
20724    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20725    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20726        if matches!(self.peek(), Token::As) {
20727            self.advance();
20728            // v7.39 (round 340, V56) — after AS the next token MUST be an
20729            // identifier. This used to return None and "let the caller
20730            // surface the error on the next expectation", but when AS is
20731            // the LAST token there is no next expectation: `SELECT 1 AS`
20732            // parsed clean and silently dropped the alias. PG rejects it.
20733            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20734                return self.expect_ident_like().map(Some);
20735            }
20736            return Err(self.err(alloc::format!(
20737                "expected an alias after AS, got {:?}",
20738                self.peek()
20739            )));
20740        }
20741        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20742        // grammar reserves a long list of follow-keywords from the
20743        // alias slot. SPG's bareword approximation: skip a small
20744        // set of idents that would otherwise be swallowed as the
20745        // table alias and break trailing clauses like CREATE
20746        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20747        // CONFLICT WHERE shapes.
20748        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20749            if is_alias_stopword(s) {
20750                return Ok(None);
20751            }
20752            return Ok(self.expect_ident_like().ok());
20753        }
20754        Ok(None)
20755    }
20756
20757    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20758    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20759        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20760        // error beats a stack overflow (an overflow aborts the
20761        // embedding host process).
20762        self.enter_nested()?;
20763        let r = self.parse_expr_inner(min_prec);
20764        self.nest_depth -= 1;
20765        r
20766    }
20767
20768    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20769    /// When the upcoming tokens form one, return the underlying
20770    /// operator token and the position just past the closing paren
20771    /// so the binary loop can dispatch on the plain operator.
20772    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20773        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20774            return None;
20775        }
20776        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20777            return None;
20778        }
20779        let mut i = self.pos + 2;
20780        // Optional schema qualifier (pg_catalog.<op> etc.).
20781        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20782            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20783        {
20784            i += 2;
20785        }
20786        let op_tok = self.tokens.get(i)?.clone();
20787        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20788            return None;
20789        }
20790        Some((i + 2, op_tok))
20791    }
20792
20793    /// PG operator symbols that lower onto function calls in
20794    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20795    /// family → regexp_like, comparison rung), `^@` (starts_with,
20796    /// comparison rung), `^` (power, tighter than `*`), `#`
20797    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20798    /// subset of the OR bits so the subtraction never borrows).
20799    fn try_symbol_operator(
20800        &mut self,
20801        lhs: &Expr,
20802        min_prec: u8,
20803    ) -> Result<Option<Expr>, ParseError> {
20804        enum Sym {
20805            Regex { ci: bool, negated: bool },
20806            Like { ci: bool, negated: bool },
20807            StartsWith,
20808            Power,
20809            Xor,
20810            RangeAdjacent,
20811        }
20812        // v7.39 (IS-precedence knife) — the low-precedence postfix
20813        // predicates ride this existing leaf call (zero new frame slots
20814        // on the nesting chain).
20815        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20816            return Ok(Some(e));
20817        }
20818        let (sym, prec): (Sym, u8) = match self.peek() {
20819            Token::Tilde => (
20820                Sym::Regex {
20821                    ci: false,
20822                    negated: false,
20823                },
20824                5,
20825            ),
20826            Token::TildeStar => (
20827                Sym::Regex {
20828                    ci: true,
20829                    negated: false,
20830                },
20831                5,
20832            ),
20833            Token::NotTilde => (
20834                Sym::Regex {
20835                    ci: false,
20836                    negated: true,
20837                },
20838                5,
20839            ),
20840            Token::NotTildeStar => (
20841                Sym::Regex {
20842                    ci: true,
20843                    negated: true,
20844                },
20845                5,
20846            ),
20847            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20848            Token::DoubleTilde => (
20849                Sym::Like {
20850                    ci: false,
20851                    negated: false,
20852                },
20853                5,
20854            ),
20855            Token::DoubleTildeStar => (
20856                Sym::Like {
20857                    ci: true,
20858                    negated: false,
20859                },
20860                5,
20861            ),
20862            Token::NotDoubleTilde => (
20863                Sym::Like {
20864                    ci: false,
20865                    negated: true,
20866                },
20867                5,
20868            ),
20869            Token::NotDoubleTildeStar => (
20870                Sym::Like {
20871                    ci: true,
20872                    negated: true,
20873                },
20874                5,
20875            ),
20876            Token::CaretAt => (Sym::StartsWith, 5),
20877            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20878            // tighter than `* / & |`, which the prec-9 rung preserves —
20879            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20880            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20881            Token::Caret => (Sym::Power, 9),
20882            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20883            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20884            Token::Hash => (Sym::Xor, 6),
20885            Token::Adjacent => (Sym::RangeAdjacent, 5),
20886            _ => return Ok(None),
20887        };
20888        if prec < min_prec {
20889            return Ok(None);
20890        }
20891        self.advance();
20892        let rhs = self.parse_expr(prec + 1)?;
20893        let out = match sym {
20894            Sym::Regex { ci, negated } => {
20895                let mut args = alloc::vec![lhs.clone(), rhs];
20896                if ci {
20897                    args.push(Expr::Literal(Literal::String(String::from("i"))));
20898                }
20899                maybe_not(
20900                    Expr::FunctionCall {
20901                        name: String::from("regexp_like"),
20902                        args,
20903                    },
20904                    negated,
20905                )
20906            }
20907            Sym::Like { ci, negated } => Expr::Like {
20908                expr: alloc::boxed::Box::new(lhs.clone()),
20909                pattern: alloc::boxed::Box::new(rhs),
20910                negated,
20911                case_insensitive: ci,
20912            },
20913            Sym::StartsWith => Expr::FunctionCall {
20914                name: String::from("starts_with"),
20915                args: alloc::vec![lhs.clone(), rhs],
20916            },
20917            Sym::Power => Expr::FunctionCall {
20918                name: String::from("power"),
20919                args: alloc::vec![lhs.clone(), rhs],
20920            },
20921            // `#` bitwise XOR — a real operator now (was desugared to
20922            // `(a|b)-(a&b)`, algebraically identical for integers but
20923            // undefined for bit strings; the direct op handles both).
20924            Sym::Xor => Expr::Binary {
20925                lhs: Box::new(lhs.clone()),
20926                op: BinOp::BitXor,
20927                rhs: Box::new(rhs),
20928            },
20929            // range `-|-` "is adjacent to" — lowered to a catalog function.
20930            Sym::RangeAdjacent => Expr::FunctionCall {
20931                name: String::from("range_adjacent"),
20932                args: alloc::vec![lhs.clone(), rhs],
20933            },
20934        };
20935        Ok(Some(out))
20936    }
20937
20938    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20939    /// predicates, moved out of the tight postfix-cast loop: PG binds
20940    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20941    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20942    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20943    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20944    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20945    /// when nothing at this position belongs to the family. Out-of-line
20946    /// (`inline(never)`): the caller sits on the per-nesting-level frame
20947    /// chain that MAX_NEST_DEPTH is tuned against.
20948    #[inline(never)]
20949    fn parse_postfix_predicate(
20950        &mut self,
20951        lhs: &Expr,
20952        min_prec: u8,
20953    ) -> Result<Option<Expr>, ParseError> {
20954        // Reached through try_symbol_operator (an existing leaf call of
20955        // the binary loop) so NO new stack slots land on the per-nesting
20956        // frame chain; the lhs clones only when a predicate actually
20957        // consumes it.
20958        match self.peek() {
20959            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20960            // comparison family rung 5 (each +1 from the pre-XOR ladder).
20961            Token::Is if min_prec <= 4 => {}
20962            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20963            Token::Not
20964                if min_prec <= 5
20965                    && matches!(
20966                        self.tokens.get(self.pos + 1),
20967                        Some(Token::Between | Token::In | Token::Like)
20968                    ) => {}
20969            Token::Not | Token::Ident(_)
20970                if min_prec <= 5
20971                    && (matches!(self.peek(), Token::Ident(s)
20972                            if s.eq_ignore_ascii_case("ilike")
20973                                || (self.mysql_dialect
20974                                    && (s.eq_ignore_ascii_case("regexp")
20975                                        || s.eq_ignore_ascii_case("rlike")))
20976                                || (s.eq_ignore_ascii_case("similar")
20977                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20978                        || (matches!(self.peek(), Token::Not)
20979                            && matches!(self.tokens.get(self.pos + 1),
20980                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20981                                    || (self.mysql_dialect
20982                                        && (s.eq_ignore_ascii_case("regexp")
20983                                            || s.eq_ignore_ascii_case("rlike")))
20984                                    || s.eq_ignore_ascii_case("similar")))) => {}
20985            _ => return Ok(None),
20986        }
20987        let mut expr = lhs.clone();
20988        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20989        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20990        if min_prec <= 4 {
20991            if matches!(self.peek(), Token::Is) {
20992                self.advance();
20993                let negated = if matches!(self.peek(), Token::Not) {
20994                    self.advance();
20995                    true
20996                } else {
20997                    false
20998                };
20999                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21000                // mailrs pg_dump.
21001                if matches!(self.peek(), Token::Distinct) {
21002                    self.advance();
21003                    if !matches!(self.peek(), Token::From) {
21004                        return Err(self.err(format!(
21005                            "expected FROM after IS{} DISTINCT, got {:?}",
21006                            if negated { " NOT" } else { "" },
21007                            self.peek()
21008                        )));
21009                    }
21010                    self.advance();
21011                    // Right-hand side: parse at the same precedence
21012                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21013                    // groups as `x IS DISTINCT FROM (a + b)`.
21014                    let rhs = self.parse_expr(5)?;
21015                    let op = if negated {
21016                        BinOp::IsNotDistinctFrom
21017                    } else {
21018                        BinOp::IsDistinctFrom
21019                    };
21020                    expr = Expr::Binary {
21021                        op,
21022                        lhs: Box::new(expr),
21023                        rhs: Box::new(rhs),
21024                    };
21025                    {
21026                        return Ok(Some(expr));
21027                    }
21028                }
21029                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21030                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21031                // Lowers onto pg_is_json(x, kind); NOT wraps the
21032                // call in a logical negation.
21033                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21034                if s.eq_ignore_ascii_case("json"))
21035                {
21036                    self.advance(); // JSON
21037                    let kind = match self.peek() {
21038                        Token::Ident(s) | Token::QuotedIdent(s)
21039                            if matches!(
21040                                s.to_ascii_lowercase().as_str(),
21041                                "value" | "object" | "array" | "scalar"
21042                            ) =>
21043                        {
21044                            let k = s.to_ascii_lowercase();
21045                            self.advance();
21046                            k
21047                        }
21048                        _ => "value".to_string(),
21049                    };
21050                    let call = Expr::FunctionCall {
21051                        name: "pg_is_json".to_string(),
21052                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21053                    };
21054                    expr = if negated {
21055                        Expr::Unary {
21056                            op: UnOp::Not,
21057                            expr: Box::new(call),
21058                        }
21059                    } else {
21060                        call
21061                    };
21062                    {
21063                        return Ok(Some(expr));
21064                    }
21065                }
21066                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21067                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21068                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21069                {
21070                    let form_kw = match self.peek() {
21071                        Token::Ident(s) | Token::QuotedIdent(s)
21072                            if matches!(
21073                                s.to_ascii_uppercase().as_str(),
21074                                "NFC" | "NFD" | "NFKC" | "NFKD"
21075                            ) && matches!(
21076                                self.tokens.get(self.pos + 1),
21077                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21078                                    if n.eq_ignore_ascii_case("normalized")
21079                            ) =>
21080                        {
21081                            Some(s.to_ascii_uppercase())
21082                        }
21083                        _ => None,
21084                    };
21085                    let bare_normalized = form_kw.is_none()
21086                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21087                        if s.eq_ignore_ascii_case("normalized"));
21088                    if form_kw.is_some() || bare_normalized {
21089                        if form_kw.is_some() {
21090                            self.advance(); // form keyword
21091                        }
21092                        self.advance(); // NORMALIZED
21093                        let mut args = alloc::vec![expr];
21094                        if let Some(f) = form_kw {
21095                            args.push(Expr::Literal(Literal::String(f)));
21096                        }
21097                        let call = Expr::FunctionCall {
21098                            name: "is_normalized".to_string(),
21099                            args,
21100                        };
21101                        expr = if negated {
21102                            Expr::Unary {
21103                                op: UnOp::Not,
21104                                expr: Box::new(call),
21105                            }
21106                        } else {
21107                            call
21108                        };
21109                        {
21110                            return Ok(Some(expr));
21111                        }
21112                    }
21113                }
21114                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21115                // three-valued boolean tests. IS TRUE/FALSE never
21116                // return NULL, so they lower to CASE forms whose
21117                // ELSE catches the NULL branch; IS UNKNOWN on a
21118                // boolean is exactly IS NULL.
21119                if matches!(self.peek(), Token::True | Token::False)
21120                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21121                {
21122                    let tok = self.advance();
21123                    let test = match tok {
21124                        Token::True => Some(true),
21125                        Token::False => Some(false),
21126                        _ => None, // UNKNOWN
21127                    };
21128                    // v7.39 (round 328, V45) — kept as what the user
21129                    // wrote. These used to be lowered here into `CASE` /
21130                    // `IS NULL`; the semantics were right but the AST no
21131                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21132                    // was echoed back as
21133                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21134                    expr = Expr::BoolTest {
21135                        expr: Box::new(expr),
21136                        value: test,
21137                        negated,
21138                    };
21139                    {
21140                        return Ok(Some(expr));
21141                    }
21142                }
21143                if !matches!(self.peek(), Token::Null) {
21144                    return Err(self.err(format!(
21145                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21146                    if negated { " NOT" } else { "" },
21147                    self.peek()
21148                )));
21149                }
21150                self.advance();
21151                expr = Expr::IsNull {
21152                    expr: Box::new(expr),
21153                    negated,
21154                };
21155                {
21156                    return Ok(Some(expr));
21157                }
21158            }
21159        }
21160        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21161        if min_prec <= 5 {
21162            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21163            // Look one token ahead so a stray `NOT` not followed by any of
21164            // these flows through to the early return below untouched.
21165            let negated = if matches!(self.peek(), Token::Not) {
21166                let next = self.tokens.get(self.pos + 1);
21167                matches!(next, Some(Token::Between | Token::In | Token::Like))
21168                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21169                    || (self.mysql_dialect
21170                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21171                    || s.eq_ignore_ascii_case("similar"))
21172            } else {
21173                false
21174            };
21175            if negated {
21176                self.advance();
21177            }
21178            if matches!(self.peek(), Token::Between) {
21179                expr = self.parse_between_tail(expr, negated)?;
21180                {
21181                    return Ok(Some(expr));
21182                }
21183            }
21184            if matches!(self.peek(), Token::In) {
21185                if self.suppress_in_tail && !negated {
21186                    // POSITION(sub IN str) — IN belongs to the
21187                    // enclosing function syntax; stop here.
21188                    {
21189                        return Ok(None);
21190                    }
21191                }
21192                expr = self.parse_in_tail(expr, negated)?;
21193                {
21194                    return Ok(Some(expr));
21195                }
21196            }
21197            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21198            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21199            // (the SQL→regex transform runs inside, in the backtracking-
21200            // friendly shape SPG's matcher needs).
21201            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21202                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21203            {
21204                self.advance(); // SIMILAR
21205                self.advance(); // TO
21206                let pattern = self.parse_expr(6)?;
21207                let mut args = alloc::vec![expr, pattern];
21208                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21209                    self.advance();
21210                    args.push(self.parse_expr(6)?);
21211                }
21212                let call = Expr::FunctionCall {
21213                    name: "__similar_to".to_string(),
21214                    args,
21215                };
21216                expr = maybe_not(call, negated);
21217                {
21218                    return Ok(Some(expr));
21219                }
21220            }
21221            if matches!(self.peek(), Token::Like) {
21222                self.advance();
21223                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21224                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21225                    expr = q;
21226                    {
21227                        return Ok(Some(expr));
21228                    }
21229                }
21230                // Pattern at the same precedence as other comparison RHSes —
21231                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21232                let mut pattern = self.parse_expr(6)?;
21233                // `ESCAPE 'c'` — rewrite a literal pattern to the
21234                // default backslash escape at parse time. Custom
21235                // escapes on non-literal patterns would need
21236                // matcher support; error honestly.
21237                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21238                    self.advance();
21239                    let esc = self.parse_expr(6)?;
21240                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21241                }
21242                expr = Expr::Like {
21243                    expr: Box::new(expr),
21244                    pattern: Box::new(pattern),
21245                    negated,
21246                    case_insensitive: false,
21247                };
21248                {
21249                    return Ok(Some(expr));
21250                }
21251            }
21252            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21253            // keyword reaches us as a plain identifier.
21254            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21255                self.advance();
21256                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21257                    expr = q;
21258                    {
21259                        return Ok(Some(expr));
21260                    }
21261                }
21262                let pattern = self.parse_expr(6)?;
21263                expr = Expr::Like {
21264                    expr: Box::new(expr),
21265                    pattern: Box::new(pattern),
21266                    negated,
21267                    case_insensitive: true,
21268                };
21269                {
21270                    return Ok(Some(expr));
21271                }
21272            }
21273            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21274            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21275            // matches case-insensitively under the default collation, so it
21276            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21277            // `~*` operator uses, wrapped in NOT when negated.
21278            if self.mysql_dialect
21279                && matches!(self.peek(), Token::Ident(s)
21280                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21281            {
21282                self.advance();
21283                let pattern = self.parse_expr(6)?;
21284                let call = Expr::FunctionCall {
21285                    name: String::from("regexp_like"),
21286                    args: alloc::vec![
21287                        expr,
21288                        pattern,
21289                        Expr::Literal(Literal::String(String::from("i"))),
21290                    ],
21291                };
21292                return Ok(Some(maybe_not(call, negated)));
21293            }
21294        }
21295        let _ = expr;
21296        Ok(None)
21297    }
21298
21299    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21300        let mut lhs = self.parse_unary()?;
21301        let mut chain_len = 0usize;
21302        loop {
21303            // OPERATOR([schema.]op) reduces to its underlying
21304            // operator token before the normal dispatch.
21305            let explicit = self.peek_explicit_operator();
21306            let dispatch = match &explicit {
21307                Some((_, tok)) => self.binop_here(tok),
21308                None => self.binop_here(self.peek()),
21309            };
21310            let Some((op, prec)) = dispatch else {
21311                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21312                // of the symbol family. `binop_here` answers None for them
21313                // because they lower onto function calls rather than a
21314                // BinOp, and the fallback below reads `self.peek()` — the
21315                // word OPERATOR, not the operator. `pg_dump` writes every
21316                // catalog predicate this way, so its first query failed
21317                // and no dump ran:
21318                //
21319                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21320                //
21321                // Collapsing the wrapper to the operator it names puts the
21322                // token where the fallback already looks.
21323                if let Some((next, op_tok)) = explicit {
21324                    self.tokens.splice(self.pos..next, [op_tok]);
21325                }
21326                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21327                    lhs = e;
21328                    chain_len += 1;
21329                    if chain_len > MAX_BINARY_CHAIN {
21330                        return Err(self.err(alloc::format!(
21331                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21332                        )));
21333                    }
21334                    continue;
21335                }
21336                break;
21337            };
21338            if prec < min_prec {
21339                break;
21340            }
21341            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21342            // iteratively but evaluates and drops recursively;
21343            // depth beyond the budget overflows worker stacks.
21344            chain_len += 1;
21345            if chain_len > MAX_BINARY_CHAIN {
21346                return Err(self.err(alloc::format!(
21347                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21348                )));
21349            }
21350            match explicit {
21351                Some((end_pos, _)) => self.pos = end_pos,
21352                None => {
21353                    self.advance();
21354                }
21355            }
21356            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21357            // ANY is a bare ident; ALL is a reserved Token. Both
21358            // require an immediate `(` to disambiguate from
21359            // identifier columns named `any` / `all`.
21360            let any_kind = match self.peek() {
21361                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21362                    Some(false)
21363                }
21364                Token::Ident(s) | Token::QuotedIdent(s)
21365                    if (s.eq_ignore_ascii_case("any")
21366                        || s.eq_ignore_ascii_case("some")
21367                        || s.eq_ignore_ascii_case("all"))
21368                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21369                {
21370                    Some(!s.eq_ignore_ascii_case("all"))
21371                }
21372                _ => None,
21373            };
21374            if let Some(is_any) = any_kind {
21375                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21376                continue;
21377            }
21378            let rhs = self.parse_expr(prec + 1)?;
21379            lhs = Expr::Binary {
21380                lhs: Box::new(lhs),
21381                op,
21382                rhs: Box::new(rhs),
21383            };
21384        }
21385        Ok(lhs)
21386    }
21387
21388    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21389    /// and the array form.
21390    ///
21391    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21392    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21393    /// this block's `Expr` temporaries and four `format!` sites slots in
21394    /// that frame on every level of `((((1))))`, which never reaches it.
21395    #[inline(never)]
21396    fn parse_any_all_rhs(
21397        &mut self,
21398        lhs: Expr,
21399        op: BinOp,
21400        is_any: bool,
21401    ) -> Result<Expr, ParseError> {
21402        self.advance(); // ident
21403        self.advance(); // (
21404        // `x op ANY (SELECT …)` — the quantified-subquery
21405        // form. `= ANY` is exactly IN; the other operators
21406        // lower onto EXISTS over the subquery as a derived
21407        // table, comparing against its single projection
21408        // aliased __v (x's columns resolve correlated).
21409        // ALL is the negated-EXISTS complement; a NULL
21410        // element makes PG return NULL where this lowering
21411        // returns true — the NOT NULL column case (the
21412        // practical one) is exact.
21413        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21414            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21415            // legal PG too (round-151 sibling). Out-of-line
21416            // (#[inline(never)] helper) — this sits on
21417            // parse_expr's recursive frame and the two-armed
21418            // SELECT temporary blew the nesting-budget stack.
21419            let mut sub = self.parse_any_all_select_body()?;
21420            if !matches!(self.peek(), Token::RParen) {
21421                return Err(self.err(alloc::format!(
21422                    "expected ')' after ANY/ALL subquery, got {:?}",
21423                    self.peek()
21424                )));
21425            }
21426            self.advance();
21427            if sub.items.len() != 1 {
21428                return Err(self.err(alloc::format!(
21429                    "ANY/ALL subquery must return one column, got {}",
21430                    sub.items.len()
21431                )));
21432            }
21433            if is_any && matches!(op, BinOp::Eq) {
21434                return Ok(Expr::InSubquery {
21435                    expr: Box::new(lhs),
21436                    subquery: Box::new(sub),
21437                    negated: false,
21438                });
21439            }
21440            // The engine's subquery resolvers materialise
21441            // the single-column result into an ARRAY the
21442            // existing AnyAll three-valued eval consumes.
21443            return Ok(Expr::AnyAll {
21444                expr: Box::new(lhs),
21445                op,
21446                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21447                is_any,
21448            });
21449        }
21450        let arr = self.parse_expr(0)?;
21451        if !matches!(self.peek(), Token::RParen) {
21452            return Err(self.err(alloc::format!(
21453                "expected ')' after ANY/ALL argument, got {:?}",
21454                self.peek()
21455            )));
21456        }
21457        self.advance();
21458        Ok(Expr::AnyAll {
21459            expr: Box::new(lhs),
21460            op,
21461            array: Box::new(arr),
21462            is_any,
21463        })
21464    }
21465
21466    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21467    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21468    #[inline(never)]
21469    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21470        self.advance();
21471        let e = self.parse_expr(9)?;
21472        Ok(build_center_call(e))
21473    }
21474
21475    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21476    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21477    /// unary minus.
21478    ///
21479    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21480    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21481    /// the Expr-sized local stays out of that frame.
21482    #[inline(never)]
21483    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21484        self.advance();
21485        let e = self.parse_expr(9)?;
21486        Ok(Expr::FunctionCall {
21487            name: alloc::string::String::from(name),
21488            args: alloc::vec![e],
21489        })
21490    }
21491
21492    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21493    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21494    #[inline(never)]
21495    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21496        self.advance();
21497        let e = self.parse_expr(9)?;
21498        Ok(Expr::FunctionCall {
21499            name: alloc::string::String::from(if vertical {
21500                "isvertical"
21501            } else {
21502                "ishorizontal"
21503            }),
21504            args: alloc::vec![e],
21505        })
21506    }
21507
21508    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21509    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21510    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21511    #[inline(never)]
21512    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21513        self.advance();
21514        let e = self.parse_expr(9)?;
21515        Ok(Expr::Cast {
21516            expr: Box::new(e),
21517            target: CastTarget::Named("binary".to_string()),
21518        })
21519    }
21520
21521    /// The prefix operators that share one shape: take the token, parse
21522    /// an operand at `prec`, wrap it.
21523    ///
21524    /// `#[inline(never)]`, and one function instead of five arms, for the
21525    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21526    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21527    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21528    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21529    /// five `Expr`-sized locals per level for them anyway.
21530    #[inline(never)]
21531    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21532        self.advance();
21533        let e = self.parse_expr(prec)?;
21534        Ok(Expr::Unary {
21535            op,
21536            expr: Box::new(e),
21537        })
21538    }
21539
21540    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21541    /// and separate from it because of the literal folding below and the
21542    /// `format!` temporaries that folding needs.
21543    #[inline(never)]
21544    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21545        self.advance();
21546        // v7.39 (round 549) — fold the sign into an integer literal that
21547        // only fits once it is negative.
21548        //
21549        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21550        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21551        // folds the sign first, so `-9223372036854775808` is a bigint
21552        // there — and `-9223372036854775808 - 1` raises "bigint out of
21553        // range" where SPG quietly answered -9223372036854775809, a value
21554        // no bigint can hold. The arithmetic itself was already checked;
21555        // only the literal's type was wrong.
21556        if let Token::Numeric(lit) = self.peek()
21557            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21558        {
21559            self.advance();
21560            return Ok(Expr::Literal(Literal::Integer(folded)));
21561        }
21562        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21563        // `<->` slotted into 5 and arithmetic shifted up).
21564        let e = self.parse_expr(9)?;
21565        Ok(Expr::Unary {
21566            op: UnOp::Neg,
21567            expr: Box::new(e),
21568        })
21569    }
21570
21571    /// tsquery `!!` prefix negation, lowered to the catalog function.
21572    /// Binds like unary minus. Out-of-line for the frame reason on
21573    /// `parse_unary_op`.
21574    #[inline(never)]
21575    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21576        self.advance();
21577        let e = self.parse_expr(9)?;
21578        Ok(Expr::FunctionCall {
21579            name: String::from("tsquery_not"),
21580            args: alloc::vec![e],
21581        })
21582    }
21583
21584    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21585        match self.peek() {
21586            // NOT binds tighter than AND / XOR / OR but looser than
21587            // comparisons — its operand takes everything ≥ the comparison
21588            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21589            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21590            // was rung 3, behaviour-identical when 3 was unused; AND now
21591            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21592            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21593            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21594            // The body is out-of-line: `parse_unary` is one of the three
21595            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21596            // inline arm here overflowed the native stack in
21597            // `nesting_budget_errors_cleanly` — the guard test caught it,
21598            // exactly as the eval-side cliff did in rounds 346 and 351.
21599            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21600                self.parse_binary_prefix()
21601            }
21602            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21603            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21604            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21605            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21606            Token::Minus => self.parse_prefix_minus(),
21607            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21608            // worked only because the lexer reads it as one signed literal;
21609            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21610            // PG18 and MariaDB take all of them. Binds like unary minus.
21611            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21612            // Bitwise NOT binds like unary minus.
21613            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21614            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21615            // "center of" operator; desugars to center(x). The whole arm
21616            // is out-of-line: parse_unary sits on the per-nesting-level
21617            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21618            // Expr-sized local may live in this frame.
21619            Token::TsMatch => self.parse_prefix_center(),
21620            // v7.39 (round 508) — the prefix operators that are named
21621            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21622            // is length. Out-of-line for the same nesting-frame reason as
21623            // parse_prefix_center — parse_unary sits on the recursive cycle
21624            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21625            // live in this frame.
21626            Token::At => self.parse_prefix_call("abs"),
21627            Token::Hash => self.parse_prefix_call("npoints"),
21628            Token::AtMinusAt => self.parse_prefix_call("length"),
21629            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21630            // "is horizontal" (lseg / line); desugars to the existing
21631            // isvertical()/ishorizontal() functions. Out-of-line for the
21632            // same nesting-frame reason as parse_prefix_center.
21633            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21634            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21635            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21636            _ => self.parse_atom(),
21637        }
21638    }
21639
21640    /// Parse a parenthesised scalar subquery body after the caller has consumed
21641    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21642    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21643    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21644    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21645    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21646    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21647    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21648    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21649    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21650    /// tips the deep-nesting test into a stack overflow).
21651    #[inline(never)]
21652    fn array_subquery_ahead(&self) -> bool {
21653        if !matches!(self.peek(), Token::LParen) {
21654            return false;
21655        }
21656        matches!(
21657            self.tokens.get(self.pos + 1),
21658            Some(Token::Select | Token::Values)
21659        ) || matches!(
21660            self.tokens.get(self.pos + 1),
21661            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21662        )
21663    }
21664
21665    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21666    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21667    /// locals stay off parse_atom's recursive frame (round 105).
21668    #[inline(never)]
21669    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21670        self.advance(); // consume `[`
21671        let mut items: Vec<Expr> = Vec::new();
21672        if !matches!(self.peek(), Token::RBracket) {
21673            loop {
21674                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21675                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21676                if matches!(self.peek(), Token::LBracket) {
21677                    items.push(self.parse_array_bracket_body()?);
21678                } else {
21679                    items.push(self.parse_expr(0)?);
21680                }
21681                match self.peek() {
21682                    Token::Comma => {
21683                        self.advance();
21684                    }
21685                    Token::RBracket => break,
21686                    other => {
21687                        return Err(self.err(alloc::format!(
21688                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21689                        )));
21690                    }
21691                }
21692            }
21693        }
21694        self.advance(); // consume `]`
21695        Ok(Expr::Array(items))
21696    }
21697
21698    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21699    /// is already consumed; the current token is `(`. Desugars to a scalar
21700    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21701    /// the subquery's single-column rows in order — reusing the existing
21702    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21703    /// keeps the large `Statement` local off parse_atom's recursive frame.
21704    #[inline(never)]
21705    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21706        self.advance(); // consume `(`
21707        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21708            if w.eq_ignore_ascii_case("with"));
21709        let sub = if is_with {
21710            self.advance(); // WITH
21711            self.parse_with_cte_then_select()?
21712        } else {
21713            self.parse_select_stmt()?
21714        };
21715        if !matches!(self.peek(), Token::RParen) {
21716            return Err(self.err(alloc::format!(
21717                "expected ')' to close ARRAY(subquery), got {:?}",
21718                self.peek()
21719            )));
21720        }
21721        self.advance(); // consume `)`
21722        // Reuse the parser to build the array_agg wrapper from the subquery's
21723        // canonical text — avoids hand-constructing the derived-table AST.
21724        let wrapper = alloc::format!(
21725            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21726        );
21727        let stmt = parse_statement(&wrapper)
21728            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21729        let Statement::Select(sel) = stmt else {
21730            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21731        };
21732        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21733    }
21734
21735    #[inline(never)]
21736    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21737        let inner = if is_with {
21738            self.advance(); // WITH
21739            self.parse_with_cte_then_select()?
21740        } else {
21741            self.parse_select_stmt()?
21742        };
21743        match self.advance() {
21744            Token::RParen => {
21745                let Statement::Select(s) = inner else {
21746                    return Err(ParseError {
21747                        message: "scalar subquery body must be a SELECT".into(),
21748                        token_pos: self.consumed_pos(),
21749                    });
21750                };
21751                Ok(Expr::ScalarSubquery(Box::new(s)))
21752            }
21753            other => Err(ParseError {
21754                message: format!("expected ')' after scalar subquery, got {other:?}"),
21755                token_pos: self.consumed_pos(),
21756            }),
21757        }
21758    }
21759
21760    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21761    /// literals. The lexer splits them into an ident + string; recombine
21762    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21763    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21764    /// frame for the `body` / `bits` strings and their char loops (the
21765    /// round-367 frame cliff, M20).
21766    #[inline(never)]
21767    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21768        let is_hex = match self.peek() {
21769            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21770            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21771            _ => return None,
21772        };
21773        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21774            return None;
21775        }
21776        self.advance();
21777        let Token::String(body) = self.advance() else {
21778            unreachable!("guarded above");
21779        };
21780        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21781        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21782        // (hex pairs, even count required — MariaDB errors on an odd
21783        // count); `b'1010'` packs its bits big-endian, left-padded to a
21784        // byte. Lower both onto the bytea cast.
21785        if self.mysql_dialect {
21786            if is_hex {
21787                if body.len() % 2 == 1 {
21788                    return Some(Err(self.err(alloc::format!(
21789                        "invalid hex string literal X'{body}': odd digit count"
21790                    ))));
21791                }
21792                for c in body.chars() {
21793                    if !c.is_ascii_hexdigit() {
21794                        return Some(Err(
21795                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21796                        ));
21797                    }
21798                }
21799                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21800            }
21801            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21802                return Some(Err(
21803                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21804                ));
21805            }
21806            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21807        }
21808        let bits = if is_hex {
21809            let mut out = String::with_capacity(body.len() * 4);
21810            for c in body.chars() {
21811                let Some(d) = c.to_digit(16) else {
21812                    return Some(Err(self.err(alloc::format!(
21813                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21814                    ))));
21815                };
21816                out.push_str(&alloc::format!("{d:04b}"));
21817            }
21818            out
21819        } else {
21820            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21821                return Some(Err(self.err(alloc::format!(
21822                    "invalid binary digit {bad:?} in B'…' bit string"
21823                ))));
21824            }
21825            body
21826        };
21827        // Route through the postfix-cast loop so a chained cast like
21828        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21829        // of erroring at the `::`.
21830        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21831        // literal keeps its exact length, while an explicit `::bit` cast is
21832        // bit(1) with pad/truncate semantics (PG).
21833        Some(self.finish_postfix_casts(Expr::Cast {
21834            expr: Box::new(Expr::Literal(Literal::String(bits))),
21835            target: CastTarget::Named("__bit_literal".to_string()),
21836        }))
21837    }
21838
21839    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21840        if let Some(res) = self.try_parse_bit_string_literal() {
21841            return res;
21842        }
21843        let tok_pos = self.pos;
21844        match self.advance() {
21845            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21846            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21847            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21848            // carrying the source mantissa + scale so no precision is lost. A
21849            // literal too wide for i128 falls back to double precision.
21850            // Out-of-line (#[inline(never)]) — this arm sits on the
21851            // parse_expr recursion chain; its expansion locals must not
21852            // widen the recursive frame (debug frame-cliff discipline).
21853            Token::Numeric(s) => match numeric_token_to_literal(s) {
21854                Ok(lit) => Ok(Expr::Literal(lit)),
21855                Err(msg) => Err(self.err(msg)),
21856            },
21857            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21858            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21859            // (the lexer only emits this token in the MySQL dialect). Lower
21860            // onto the existing bytea cast; out-of-line to keep this arm off
21861            // the parse recursion frame.
21862            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21863            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21864            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21865            Token::Null => Ok(Expr::Literal(Literal::Null)),
21866            // v6.1.1 — `$N` placeholder. The actual Value lookup
21867            // happens in the engine eval path against the prepared-
21868            // statement bind buffer.
21869            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21870            Token::LParen => {
21871                // v4.10: `(SELECT ...)` in expression position is a
21872                // scalar subquery; otherwise it's a parenthesised
21873                // expression. Peek for SELECT keyword to dispatch.
21874                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21875                // lexes as Ident("with") (not a reserved token). The subquery body
21876                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21877                // so its large `Statement` local stays out of parse_atom's stack
21878                // frame — parse_atom is on the recursive `((…))` cycle and the
21879                // nesting budget is tuned to its frame size).
21880                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21881                    if s.eq_ignore_ascii_case("with"));
21882                if matches!(self.peek(), Token::Select) || is_with {
21883                    self.parse_paren_scalar_subquery(is_with)
21884                } else {
21885                    let e = self.parse_expr(0)?;
21886                    // `(a, b, …)` — a row constructor. Valid only
21887                    // in front of a comparison operator or [NOT]
21888                    // IN; both expand at parse time (lexicographic
21889                    // comparison / OR'd row equalities).
21890                    if matches!(self.peek(), Token::Comma) {
21891                        let mut row = alloc::vec![e];
21892                        while matches!(self.peek(), Token::Comma) {
21893                            self.advance();
21894                            row.push(self.parse_expr(0)?);
21895                        }
21896                        if !matches!(self.peek(), Token::RParen) {
21897                            return Err(self.err(alloc::format!(
21898                                "expected ')' after row constructor, got {:?}",
21899                                self.peek()
21900                            )));
21901                        }
21902                        self.advance();
21903                        // A bare `(a, b, …)` row constructor can carry postfix
21904                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21905                        // early return here skips parse_atom's tail postfix
21906                        // pass, so fold casts in explicitly. For the
21907                        // comparison / predicate forms nothing postfix follows,
21908                        // so this is a no-op.
21909                        return self
21910                            .parse_row_comparison_tail(row)
21911                            .and_then(|e| self.finish_postfix_casts(e));
21912                    }
21913                    match self.advance() {
21914                        Token::RParen => Ok(e),
21915                        other => Err(ParseError {
21916                            message: format!("expected ')', got {other:?}"),
21917                            token_pos: self.consumed_pos(),
21918                        }),
21919                    }
21920                }
21921            }
21922            Token::LBracket => self.parse_vector_literal_body(),
21923            Token::Extract => self.parse_extract_atom(),
21924            Token::Interval => self.parse_interval_atom(),
21925            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21926            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21927            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21928            // expression position calling the PG `left(string, n)` /
21929            // `right(string, n)` function; rebuild the AST as a regular
21930            // function call so the engine's apply_function dispatch picks
21931            // it up. Delegated to a #[inline(never)] helper so its locals
21932            // don't bloat this recursive `parse_atom` frame (the nesting
21933            // budget in `enter_nested` is tuned to parse_atom's size).
21934            Token::Left if matches!(self.peek(), Token::LParen) => {
21935                self.parse_lr_string_function_call("left")
21936            }
21937            Token::Right if matches!(self.peek(), Token::LParen) => {
21938                self.parse_lr_string_function_call("right")
21939            }
21940            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21941            // token; we match on the bare ident. NOT is a token
21942            // (consumed in the comparison rung), but `EXISTS (...)`
21943            // at the top of an expression starts here.
21944            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21945                self.parse_exists_atom(false)
21946            }
21947            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21948            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21949            // CASE is a bare ident; we dispatch on lowercase match.
21950            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21951                self.parse_case_atom()
21952            }
21953            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21954            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21955            // '…'`. Lower onto the ::cast node so the existing
21956            // runtime text→date/timestamp paths do the parsing. The
21957            // string must follow immediately, else the ident stays a
21958            // plain column reference.
21959            Token::Ident(s)
21960                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21961                    && matches!(self.peek(), Token::String(_)) =>
21962            {
21963                let target =
21964                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21965                let Token::String(lit) = self.advance() else {
21966                    unreachable!("peek guaranteed a string token");
21967                };
21968                Ok(Expr::Cast {
21969                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21970                    target,
21971                })
21972            }
21973            // v7.39 (round 221) — the SQL-standard long spellings:
21974            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21975            // TIME ZONE '…'`. Consume the modifier and lower to the same
21976            // typed-literal cast (`timetz` / `timestamptz` for WITH).
21977            Token::Ident(s)
21978                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21979                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21980                        || w.eq_ignore_ascii_case("without"))
21981                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21982                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21983                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21984            {
21985                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21986                self.advance(); // WITH / WITHOUT
21987                self.advance(); // TIME
21988                self.advance(); // ZONE
21989                let Token::String(lit) = self.advance() else {
21990                    unreachable!("guard checked a string token");
21991                };
21992                let base = s.to_ascii_lowercase();
21993                let target = match (base.as_str(), with_tz) {
21994                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21995                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21996                    (_, true) => CastTarget::Timestamptz,
21997                    (_, false) => CastTarget::Timestamp,
21998                };
21999                Ok(Expr::Cast {
22000                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22001                    target,
22002                })
22003            }
22004            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22005            // gathers the subquery's single-column rows (in its row order)
22006            // into an array. Desugared to `array_agg` over the subquery as a
22007            // derived table; out-of-line to keep parse_atom's frame small (it
22008            // sits on the recursive nesting-budget cycle).
22009            Token::Ident(s) | Token::QuotedIdent(s)
22010                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22011            {
22012                self.parse_array_subquery()
22013            }
22014            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22015            // is not a reserved token; we match by case-insensitive
22016            // ident. The opening `[` must follow immediately. v7.39 (read01
22017            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22018            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22019            Token::Ident(s) | Token::QuotedIdent(s)
22020                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22021            {
22022                self.parse_array_literal_body()
22023            }
22024            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22025            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22026            // We special-case before the generic ident dispatch so
22027            // the AGAINST clause never reaches the function-call
22028            // loop (which would mis-read `(cols) AGAINST` as a
22029            // call with no trailing modifier). The shape is
22030            // rewritten to a Boolean OR over per-column
22031            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22032            // term)` so the existing FTS evaluator handles
22033            // semantics — the fulltext-GIN built at CREATE TABLE
22034            // time is currently a "real index that survives dump
22035            // round-trip"; the planner hook that actually uses
22036            // it for posting-list intersection lands in a later
22037            // sub-phase (Phase 2.2b) without touching this surface.
22038            Token::Ident(s) | Token::QuotedIdent(s)
22039                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22040            {
22041                self.parse_match_against_atom()
22042            }
22043            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22044            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22045            // alias names in expression context too. `release` appears
22046            // in sentori `0003_partition_events.sql` as both a column
22047            // reference (SELECT … release …) and an INSERT column list
22048            // entry. Mirrors `expect_ident_like`'s expansion of the
22049            // identifier set.
22050            other if unreserved_keyword_text(&other).is_some() => {
22051                let s = unreserved_keyword_text(&other).unwrap();
22052                self.finish_ident_atom(s)
22053            }
22054            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22055            // only inside `SET` before, so `SELECT @@autocommit` — which
22056            // every MySQL connector asks at handshake — was a parse error.
22057            // MariaDB accepts the bare, `@@session.` and `@@global.`
22058            // spellings alike and answers from the session's own value.
22059            Token::SessionVar(v) => {
22060                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22061                // has nothing to do with a `@@` engine setting: its own
22062                // per-session namespace, and an unset one reads NULL instead
22063                // of raising. Stripping every `@` (as this did) made `@x` and
22064                // `@@x` the same node, so `SELECT @x` answered "Unknown
22065                // system variable".
22066                Ok(variable_ref_atom(&v))
22067            }
22068            other => Err(ParseError {
22069                message: format!("unexpected token {other:?} in expression"),
22070                token_pos: tok_pos,
22071            }),
22072        }
22073        // After parsing the atom, fold any postfix `::vector` casts.
22074        .and_then(|atom| self.finish_postfix_casts(atom))
22075    }
22076
22077    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22078    /// Both bind tighter than any binary op.
22079    /// Shared cast-target parser for postfix `::TYPE` and the
22080    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22081    /// If the next tokens are `( N )`, consume them and return the canonical
22082    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22083    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22084    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22085        if !matches!(self.peek(), Token::LParen) {
22086            return None;
22087        }
22088        self.advance(); // (
22089        let n = match self.advance() {
22090            Token::Integer(n) => n,
22091            _ => return Some(base.to_string()), // malformed → drop precision
22092        };
22093        if matches!(self.peek(), Token::RParen) {
22094            self.advance();
22095        }
22096        Some(alloc::format!("{base}({n})"))
22097    }
22098
22099    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22100        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22101        // schema-qualifies every cast target, and `pg_catalog.X` names
22102        // exactly the builtin type X. Consume the qualifier and let
22103        // the ordinary target parse decide.
22104        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22105            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22106        {
22107            self.advance();
22108            self.advance();
22109        }
22110        let target = match self.advance() {
22111            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22112                "int" | "integer" | "int4" => {
22113                    if matches!(self.peek(), Token::LBracket)
22114                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22115                    {
22116                        self.advance();
22117                        self.advance();
22118                        CastTarget::IntArray
22119                    } else {
22120                        CastTarget::Int
22121                    }
22122                }
22123                "bigint" | "int8" => {
22124                    if matches!(self.peek(), Token::LBracket)
22125                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22126                    {
22127                        self.advance();
22128                        self.advance();
22129                        CastTarget::BigIntArray
22130                    } else {
22131                        CastTarget::BigInt
22132                    }
22133                }
22134                "float" | "double" => CastTarget::Float,
22135                "text" => {
22136                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22137                    if matches!(self.peek(), Token::LBracket)
22138                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22139                    {
22140                        self.advance();
22141                        self.advance();
22142                        CastTarget::TextArray
22143                    } else {
22144                        CastTarget::Text
22145                    }
22146                }
22147                "bool" | "boolean" => CastTarget::Bool,
22148                "vector" => CastTarget::Vector,
22149                "date" => CastTarget::Date,
22150                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22151                // seconds precision through the Named path (the engine rounds
22152                // the sub-second field); bare `::timestamp` keeps the fast arm.
22153                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22154                    Some(named) => CastTarget::Named(named),
22155                    None => CastTarget::Timestamp,
22156                },
22157                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22158                    Some(named) => CastTarget::Named(named),
22159                    None => CastTarget::Timestamptz,
22160                },
22161                "interval" => CastTarget::Interval,
22162                "json" => CastTarget::Json,
22163                "jsonb" => CastTarget::Jsonb,
22164                // v7.39 (round 694) — these have dedicated CastTarget
22165                // variants, so they never reached the postfix `[]` handling
22166                // further down and `::regtype[]` was a SYNTAX error at the
22167                // `]`. PG has an array type for every scalar; take the
22168                // suffix here and hand the canonical `<ty>_array` name to
22169                // the engine, the same shape every other array cast uses.
22170                "regtype" if self.peek_postfix_array_brackets() => {
22171                    self.advance();
22172                    self.advance();
22173                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22174                }
22175                "regclass" if self.peek_postfix_array_brackets() => {
22176                    self.advance();
22177                    self.advance();
22178                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22179                }
22180                "regtype" => CastTarget::RegType,
22181                "regclass" => CastTarget::RegClass,
22182                // v7.12.0 — `::tsvector` / `::tsquery`.
22183                // Engine decodes the LHS text via the PG
22184                // external form parser.
22185                // v7.39 (round 352, M8) — MySQL's own cast targets.
22186                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22187                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22188                // such type, so they are taken only in that dialect and
22189                // fall through to the "type does not exist" arm otherwise.
22190                "signed" | "unsigned" if self.mysql_dialect => {
22191                    if matches!(self.peek(), Token::Ident(k)
22192                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22193                    {
22194                        self.advance();
22195                    }
22196                    CastTarget::Named(s.to_ascii_lowercase())
22197                }
22198                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22199                // in MySQL: MariaDB answers '123' where the SQL-standard
22200                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22201                // Truncating a number to its first digit is a wrong answer
22202                // with no error, so the MySQL session gets MySQL's reading.
22203                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22204                    CastTarget::Text
22205                }
22206                "tsvector" => CastTarget::TsVector,
22207                "tsquery" => CastTarget::TsQuery,
22208                // v7.17.0 — `::uuid`. Engine decodes the LHS
22209                // text via `spg_storage::parse_uuid_str`.
22210                "uuid" => CastTarget::Uuid,
22211                // v7.18 — `::bytea`. Engine decodes the LHS
22212                // text via the PG hex form (`'\xdeadbeef'`)
22213                // or escape form (`'\\x05\\x00'`). Closes
22214                // mailrs D-pre #3 reverse-acceptance gap.
22215                "bytea" => CastTarget::Bytea,
22216                // v7.37.5 ship triage — generic typed-cast escape.
22217                // Anything the long-tail PG type ident table knows
22218                // about(network/bit/geometry/multirange/etc.)flows
22219                // through `CastTarget::Named(canonical)`; the engine
22220                // resolves via `column_type_to_data_type` and dispatches
22221                // through the typed `coerce_value` path. Truly
22222                // unrecognised idents still hit the error arm below
22223                // because the engine rejects them.
22224                other => {
22225                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22226                    // `::varchar(255)`, etc. Capture into the canonical
22227                    // `name(p,s)` form so `type_name_to_data_type` can
22228                    // reconstruct the `DataType::Numeric { precision,
22229                    // scale }` (and similar param-carrying types).
22230                    let mut name = other.to_string();
22231                    // v7.39 (round 281) — `::bit varying(3)` is two
22232                    // words; fold the tail in so the typmod reaches the
22233                    // type resolver instead of tripping the parser.
22234                    if name.eq_ignore_ascii_case("bit")
22235                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22236                    {
22237                        self.advance();
22238                        name = alloc::string::String::from("varbit");
22239                    }
22240                    // v7.39 (round 613) — `::character varying` is the same
22241                    // two-word shape and had no fold, so the `varying` was
22242                    // left behind and the cast became a bare `character`,
22243                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22244                    // `a` where PG answers `ab`. Silently, and for a spelling
22245                    // pg_dump writes.
22246                    if name.eq_ignore_ascii_case("character")
22247                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22248                    {
22249                        self.advance();
22250                        name = alloc::string::String::from("varchar");
22251                    }
22252                    if matches!(self.peek(), Token::LParen) {
22253                        let mut buf = alloc::string::String::from("(");
22254                        let mut depth = 0usize;
22255                        loop {
22256                            match self.advance() {
22257                                Token::LParen => {
22258                                    depth += 1;
22259                                    if depth > 1 {
22260                                        buf.push('(');
22261                                    }
22262                                }
22263                                Token::RParen => {
22264                                    depth -= 1;
22265                                    if depth == 0 {
22266                                        buf.push(')');
22267                                        break;
22268                                    }
22269                                    buf.push(')');
22270                                }
22271                                Token::Comma => buf.push(','),
22272                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22273                                // v7.39 (round 273) — a minus used to fall
22274                                // into the catch-all below and vanish, so
22275                                // `::numeric(10,-2)` reached the engine as
22276                                // the text `numeric(10,2)` and silently
22277                                // rounded to two DECIMALS instead of to
22278                                // hundreds. A dropped token is not a
22279                                // no-op when it carries a sign.
22280                                Token::Minus => buf.push('-'),
22281                                Token::Eof => break,
22282                                _ => {}
22283                            }
22284                        }
22285                        name.push_str(&buf);
22286                    }
22287                    // Optional postfix `[]` widens to the array form —
22288                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22289                    // The engine's `type_name_to_data_type` recognises
22290                    // the canonical `<ty>_array` form.
22291                    if matches!(self.peek(), Token::LBracket)
22292                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22293                    {
22294                        self.advance();
22295                        self.advance();
22296                        name.push_str("_array");
22297                    }
22298                    CastTarget::Named(name)
22299                }
22300            },
22301            Token::Interval => CastTarget::Interval,
22302            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22303            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22304            // = char(1)); other quoted names resolve like idents.
22305            Token::QuotedIdent(q) => {
22306                if q.eq_ignore_ascii_case("char") {
22307                    CastTarget::Named("char1".into())
22308                } else {
22309                    CastTarget::Named(q.to_ascii_lowercase())
22310                }
22311            }
22312            other => {
22313                return Err(ParseError {
22314                    message: format!("expected type ident after `::`, got {other:?}"),
22315                    token_pos: self.consumed_pos(),
22316                });
22317            }
22318        };
22319        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22320        // target to its array sibling. Closed-enum arms (Bool /
22321        // SmallInt / Numeric / Float / Date / …) didn't carry the
22322        // explicit widening that Text / Int / BigInt did, so
22323        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22324        // error. The widening here mirrors the per-arm Text /
22325        // Int / BigInt logic above + folds the new ζ-A first-class
22326        // types through `CastTarget::Named("<ty>_array")`.
22327        if matches!(self.peek(), Token::LBracket)
22328            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22329        {
22330            let widened = match &target {
22331                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22332                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22333                // v7.39 (round 326, V43) — the two temporal types stay
22334                // distinct. Both used to widen to `timestamptz_array`, so
22335                // `::timestamp[]` named the wrong target in its own error
22336                // message and lost the zone-less identity on the way.
22337                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22338                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22339                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22340                CastTarget::Json | CastTarget::Jsonb => {
22341                    Some(CastTarget::Named("jsonb_array".to_string()))
22342                }
22343                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22344                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22345                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22346                CastTarget::Named(name) => {
22347                    let mut a = name.clone();
22348                    a.push_str("_array");
22349                    Some(CastTarget::Named(a))
22350                }
22351                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22352                // RegType / RegClass / TextArray / IntArray /
22353                // BigIntArray already finalised — leave as is.
22354                _ => None,
22355            };
22356            if let Some(w) = widened {
22357                self.advance();
22358                self.advance();
22359                return Ok(w);
22360            }
22361        }
22362        Ok(target)
22363    }
22364
22365    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22366        loop {
22367            // v7.38 (read01, T9) — composite field access `(expr).field`.
22368            // A bare `a.b` is consumed as a qualified column inside the ident
22369            // atom, so a Dot only survives to this postfix position when the
22370            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22371            // `.*` whole-row expansion is not handled here (projection-level).
22372            if matches!(self.peek(), Token::Dot)
22373                && matches!(
22374                    self.tokens.get(self.pos + 1),
22375                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22376                )
22377            {
22378                self.advance(); // .
22379                let field = match self.advance() {
22380                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22381                    other => {
22382                        return Err(
22383                            self.err(format!("expected a field name after '.', got {other:?}"))
22384                        );
22385                    }
22386                };
22387                expr = Expr::FieldAccess {
22388                    base: Box::new(expr),
22389                    field,
22390                };
22391                continue;
22392            }
22393            if matches!(self.peek(), Token::DoubleColon) {
22394                self.advance();
22395                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22396                // target set to include INTERVAL (reserved Token),
22397                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22398                // mailrs follow-up H3a + H3b.
22399                let target = self.parse_cast_target()?;
22400                expr = Expr::Cast {
22401                    expr: Box::new(expr),
22402                    target,
22403                };
22404                continue;
22405            }
22406            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22407            // returns NULL for out-of-range. Multiple subscripts
22408            // chain: `a[i][j]` parses left-to-right.
22409            if matches!(self.peek(), Token::LBracket) {
22410                self.advance();
22411                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22412                // bare index stays a subscript.
22413                let lo = if matches!(self.peek(), Token::Colon) {
22414                    None
22415                } else {
22416                    Some(self.parse_expr(0)?)
22417                };
22418                if matches!(self.peek(), Token::Colon) {
22419                    self.advance();
22420                    let hi = if matches!(self.peek(), Token::RBracket) {
22421                        None
22422                    } else {
22423                        Some(Box::new(self.parse_expr(0)?))
22424                    };
22425                    if !matches!(self.peek(), Token::RBracket) {
22426                        return Err(self.err(alloc::format!(
22427                            "expected ']' after array slice, got {:?}",
22428                            self.peek()
22429                        )));
22430                    }
22431                    self.advance();
22432                    expr = Expr::ArraySlice {
22433                        target: Box::new(expr),
22434                        lo: lo.map(Box::new),
22435                        hi,
22436                    };
22437                    continue;
22438                }
22439                let index = lo.expect("non-colon branch parsed an index");
22440                if !matches!(self.peek(), Token::RBracket) {
22441                    return Err(self.err(alloc::format!(
22442                        "expected ']' after array index, got {:?}",
22443                        self.peek()
22444                    )));
22445                }
22446                self.advance();
22447                expr = Expr::ArraySubscript {
22448                    target: Box::new(expr),
22449                    index: Box::new(index),
22450                };
22451                continue;
22452            }
22453            // `expr AT TIME ZONE zone` — lowers to PG's own function
22454            // form timezone(zone, expr); the scalar implements the
22455            // offset shift (named zones error there — no tzdata).
22456            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22457                && matches!(self.tokens.get(self.pos + 1),
22458                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22459                && matches!(self.tokens.get(self.pos + 2),
22460                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22461            {
22462                self.advance(); // AT
22463                self.advance(); // TIME
22464                self.advance(); // ZONE
22465                // Zone at comparison precedence so AND/OR stay out.
22466                let zone = self.parse_expr(6)?;
22467                expr = Expr::FunctionCall {
22468                    name: "timezone".to_string(),
22469                    args: alloc::vec![zone, expr],
22470                };
22471                continue;
22472            }
22473            // `expr COLLATE "name"` — SPG's single text ordering IS
22474            // byte order, i.e. the C collation. The byte-order
22475            // spellings absorb as no-ops; a locale collation would
22476            // silently sort differently from PG, so it errors
22477            // honestly instead.
22478            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22479                self.advance();
22480                let mut cname = match self.advance() {
22481                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22482                    other => {
22483                        return Err(self.err(alloc::format!(
22484                            "expected collation name after COLLATE, got {other:?}"
22485                        )));
22486                    }
22487                };
22488                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22489                // is how `pg_dump` writes the default one:
22490                // `… COLLATE pg_catalog.default`. Reading a single token
22491                // left the SCHEMA as the name, so the clause was refused
22492                // as an unsupported locale collation and no dump ran.
22493                if matches!(self.peek(), Token::Dot) {
22494                    self.advance();
22495                    cname = match self.advance() {
22496                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22497                        // `default` lexes as a KEYWORD, and it is the name
22498                        // pg_dump writes — the same trap round 535 hit with
22499                        // TABLE / INDEX / FULL.
22500                        Token::Default => alloc::string::String::from("default"),
22501                        other => {
22502                            return Err(self.err(alloc::format!(
22503                                "expected collation name after COLLATE, got {other:?}"
22504                            )));
22505                        }
22506                    };
22507                }
22508                let lc = cname.to_ascii_lowercase();
22509                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22510                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22511                // family / `binary`) forces byte-wise, which is exactly
22512                // what `BINARY expr` does — lower onto that so every fold
22513                // site (comparison, LIKE, ORDER BY) suppresses via
22514                // `is_binary_coerced`. A `_ci` family override folds, and
22515                // under the MySQL dialect the default already folds, so it
22516                // absorbs as a no-op; likewise the C / byte-order spellings.
22517                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22518                    expr = Expr::Cast {
22519                        expr: alloc::boxed::Box::new(expr),
22520                        target: CastTarget::Named("binary".to_string()),
22521                    };
22522                    continue;
22523                }
22524                let mysql_ci = self.mysql_dialect
22525                    && (lc.ends_with("_ci")
22526                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22527                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22528                // goes to the lowering channel, the byte-order spellings
22529                // included. Round 691 recorded only the names the old
22530                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22531                // absorbed as a no-op — and once a column could declare a
22532                // collation, absorbing the clause meant the COLUMN's
22533                // collation won where the query had asked for bytes.
22534                if self.in_order_by_key && !mysql_ci {
22535                    self.order_key_collation = Some(cname);
22536                    continue;
22537                }
22538                if !matches!(
22539                    lc.as_str(),
22540                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22541                ) && !mysql_ci
22542                {
22543                    return Err(self.err(alloc::format!(
22544                        "COLLATE {cname:?}: SPG orders text by bytes (the C \
22545                         collation); locale collations are not supported yet — \
22546                         use COLLATE \"C\" or drop the clause"
22547                    )));
22548                }
22549                continue;
22550            }
22551            return Ok(expr);
22552        }
22553    }
22554
22555    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22556    /// the first token that is not one. Schema qualifiers collapse to the
22557    /// last part, which is what every other name path here does (SPG is
22558    /// single-schema).
22559    fn take_comma_separated_names(&mut self) -> Vec<String> {
22560        let mut out = Vec::new();
22561        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22562            self.advance();
22563            let mut last = n;
22564            while matches!(self.peek(), Token::Dot) {
22565                self.advance();
22566                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22567                    last = t;
22568                }
22569            }
22570            out.push(last);
22571            if matches!(self.peek(), Token::Comma) {
22572                self.advance();
22573            } else {
22574                break;
22575            }
22576        }
22577        out
22578    }
22579
22580    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22581    ///
22582    /// The general cast-target path tests this inline; the types with their
22583    /// own `CastTarget` variant need it as a guard on their match arm,
22584    /// which is what this exists for.
22585    fn peek_postfix_array_brackets(&self) -> bool {
22586        matches!(self.peek(), Token::LBracket)
22587            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22588    }
22589
22590    /// Parse the operator tail after a `(a, b, …)` row constructor
22591    /// and expand at parse time. `=` is the conjunction of element
22592    /// equalities; `<>` its negation; the order operators expand
22593    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22594    /// equalities. Anything else (a bare row value, a subquery
22595    /// RHS) errors honestly — SPG has no composite runtime value.
22596    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22597        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22598            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22599                lhs: Box::new(l.clone()),
22600                op: BinOp::Eq,
22601                rhs: Box::new(r.clone()),
22602            });
22603            let first = it.next().expect("row has at least two elements");
22604            it.fold(first, |acc, e| Expr::Binary {
22605                lhs: Box::new(acc),
22606                op: BinOp::And,
22607                rhs: Box::new(e),
22608            })
22609        }
22610        // Lexicographic (a,b) OP (c,d):
22611        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22612        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22613            if lhs.len() == 1 {
22614                return Expr::Binary {
22615                    lhs: Box::new(lhs[0].clone()),
22616                    op: last,
22617                    rhs: Box::new(rhs[0].clone()),
22618                };
22619            }
22620            let head_strict = Expr::Binary {
22621                lhs: Box::new(lhs[0].clone()),
22622                op: strict,
22623                rhs: Box::new(rhs[0].clone()),
22624            };
22625            let head_eq = Expr::Binary {
22626                lhs: Box::new(lhs[0].clone()),
22627                op: BinOp::Eq,
22628                rhs: Box::new(rhs[0].clone()),
22629            };
22630            Expr::Binary {
22631                lhs: Box::new(head_strict),
22632                op: BinOp::Or,
22633                rhs: Box::new(Expr::Binary {
22634                    lhs: Box::new(head_eq),
22635                    op: BinOp::And,
22636                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22637                }),
22638            }
22639        }
22640        let negated_in = if matches!(self.peek(), Token::Not)
22641            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22642        {
22643            self.advance();
22644            true
22645        } else {
22646            false
22647        };
22648        if matches!(self.peek(), Token::In) {
22649            self.advance();
22650            if !matches!(self.peek(), Token::LParen) {
22651                return Err(self.err(alloc::format!(
22652                    "expected '(' after row IN, got {:?}",
22653                    self.peek()
22654                )));
22655            }
22656            self.advance();
22657            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22658            // not a list of literal rows. Row-vs-list decomposes to
22659            // OR-of-AND above, but the subquery's rows are only known at
22660            // runtime, so keep it as a RowInSubquery node.
22661            if matches!(self.peek(), Token::Select) {
22662                let inner = self.parse_select_stmt()?;
22663                if !matches!(self.peek(), Token::RParen) {
22664                    return Err(self.err(alloc::format!(
22665                        "expected ')' after row IN-subquery, got {:?}",
22666                        self.peek()
22667                    )));
22668                }
22669                self.advance();
22670                let Statement::Select(s) = inner else {
22671                    unreachable!("parse_select_stmt always returns Statement::Select")
22672                };
22673                return Ok(Expr::RowInSubquery {
22674                    row,
22675                    subquery: Box::new(s),
22676                    negated: negated_in,
22677                });
22678            }
22679            let mut alternatives: Vec<Expr> = Vec::new();
22680            loop {
22681                // Optional ROW keyword before the paren row.
22682                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22683                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22684                {
22685                    self.advance();
22686                }
22687                if !matches!(self.peek(), Token::LParen) {
22688                    return Err(self.err(alloc::format!(
22689                        "expected '(' to open a row inside IN, got {:?}",
22690                        self.peek()
22691                    )));
22692                }
22693                self.advance();
22694                let mut rhs = alloc::vec![self.parse_expr(0)?];
22695                while matches!(self.peek(), Token::Comma) {
22696                    self.advance();
22697                    rhs.push(self.parse_expr(0)?);
22698                }
22699                if !matches!(self.peek(), Token::RParen) {
22700                    return Err(self.err(alloc::format!(
22701                        "expected ')' after row inside IN, got {:?}",
22702                        self.peek()
22703                    )));
22704                }
22705                self.advance();
22706                if rhs.len() != row.len() {
22707                    return Err(self.err(alloc::format!(
22708                        "row IN arity mismatch: left has {}, right has {}",
22709                        row.len(),
22710                        rhs.len()
22711                    )));
22712                }
22713                alternatives.push(row_eq(&row, &rhs));
22714                if matches!(self.peek(), Token::Comma) {
22715                    self.advance();
22716                    continue;
22717                }
22718                break;
22719            }
22720            if !matches!(self.peek(), Token::RParen) {
22721                return Err(self.err(alloc::format!(
22722                    "expected ')' to close row IN list, got {:?}",
22723                    self.peek()
22724                )));
22725            }
22726            self.advance();
22727            let mut it = alternatives.into_iter();
22728            let first = it.next().expect("IN list has at least one row");
22729            let combined = it.fold(first, |acc, e| Expr::Binary {
22730                lhs: Box::new(acc),
22731                op: BinOp::Or,
22732                rhs: Box::new(e),
22733            });
22734            return Ok(maybe_not(combined, negated_in));
22735        }
22736        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22737        // two periods share at least one time point. Each pair is
22738        // normalised with least/greatest (PG accepts the endpoints
22739        // in either order), then lowered to the standard
22740        // `start1 < end2 AND start2 < end1` form.
22741        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22742            if row.len() != 2 {
22743                return Err(self.err(alloc::format!(
22744                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22745                    row.len()
22746                )));
22747            }
22748            self.advance();
22749            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22750                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22751            {
22752                self.advance();
22753            }
22754            if !matches!(self.peek(), Token::LParen) {
22755                return Err(self.err(alloc::format!(
22756                    "expected '(' after OVERLAPS, got {:?}",
22757                    self.peek()
22758                )));
22759            }
22760            self.advance();
22761            let r0 = self.parse_expr(0)?;
22762            if !matches!(self.peek(), Token::Comma) {
22763                return Err(self.err(alloc::format!(
22764                    "OVERLAPS needs (start, end) on the right, got {:?}",
22765                    self.peek()
22766                )));
22767            }
22768            self.advance();
22769            let r1 = self.parse_expr(0)?;
22770            if !matches!(self.peek(), Token::RParen) {
22771                return Err(self.err(alloc::format!(
22772                    "expected ')' after OVERLAPS pair, got {:?}",
22773                    self.peek()
22774                )));
22775            }
22776            self.advance();
22777            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22778                name: String::from(name),
22779                args: alloc::vec![a.clone(), b.clone()],
22780            };
22781            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22782                lhs: Box::new(lhs),
22783                op: BinOp::Lt,
22784                rhs: Box::new(rhs),
22785            };
22786            return Ok(Expr::Binary {
22787                lhs: Box::new(lt(
22788                    pair_fn("least", &row[0], &row[1]),
22789                    pair_fn("greatest", &r0, &r1),
22790                )),
22791                op: BinOp::And,
22792                rhs: Box::new(lt(
22793                    pair_fn("least", &r0, &r1),
22794                    pair_fn("greatest", &row[0], &row[1]),
22795                )),
22796            });
22797        }
22798        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22799        // PG, `IS NULL` is true only when EVERY field is NULL, and
22800        // `IS NOT NULL` is true only when every field is non-NULL — the
22801        // latter is NOT the negation of the former (a mixed row is
22802        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22803        // which reproduces exactly that all-fields semantics.
22804        if matches!(self.peek(), Token::Is) {
22805            self.advance();
22806            let negated = if matches!(self.peek(), Token::Not) {
22807                self.advance();
22808                true
22809            } else {
22810                false
22811            };
22812            if !matches!(self.peek(), Token::Null) {
22813                return Err(self.err(alloc::format!(
22814                    "expected NULL after row IS [NOT], got {:?}",
22815                    self.peek()
22816                )));
22817            }
22818            self.advance();
22819            let mut it = row.iter().map(|e| Expr::IsNull {
22820                expr: Box::new(e.clone()),
22821                negated,
22822            });
22823            let first = it.next().expect("row has at least two elements");
22824            return Ok(it.fold(first, |acc, e| Expr::Binary {
22825                lhs: Box::new(acc),
22826                op: BinOp::And,
22827                rhs: Box::new(e),
22828            }));
22829        }
22830        let op = match self.peek() {
22831            Token::Eq => BinOp::Eq,
22832            Token::NotEq => BinOp::NotEq,
22833            Token::Lt => BinOp::Lt,
22834            Token::LtEq => BinOp::LtEq,
22835            Token::Gt => BinOp::Gt,
22836            Token::GtEq => BinOp::GtEq,
22837            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22838            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22839            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22840            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22841            // (`::text`, `.field`) applies at the caller just as it does for the
22842            // ROW(...) node. All the comparison / predicate forms returned above.
22843            _ => {
22844                return Ok(Expr::FunctionCall {
22845                    name: String::from("row"),
22846                    args: row,
22847                });
22848            }
22849        };
22850        self.advance();
22851        // Optional ROW keyword before the paren row.
22852        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22853            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22854        {
22855            self.advance();
22856        }
22857        if !matches!(self.peek(), Token::LParen) {
22858            return Err(self.err(alloc::format!(
22859                "expected '(' to open the right-hand row, got {:?}",
22860                self.peek()
22861            )));
22862        }
22863        self.advance();
22864        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22865        // subquery. Kept as a node (the subquery's row is a runtime value);
22866        // the literal-RHS form below still decomposes at parse time.
22867        if matches!(self.peek(), Token::Select) {
22868            let inner = self.parse_select_stmt()?;
22869            if !matches!(self.peek(), Token::RParen) {
22870                return Err(self.err(alloc::format!(
22871                    "expected ')' after row comparison subquery, got {:?}",
22872                    self.peek()
22873                )));
22874            }
22875            self.advance();
22876            let Statement::Select(s) = inner else {
22877                unreachable!("parse_select_stmt always returns Statement::Select")
22878            };
22879            return Ok(Expr::RowCmpSubquery {
22880                row,
22881                op,
22882                subquery: Box::new(s),
22883            });
22884        }
22885        let mut rhs = alloc::vec![self.parse_expr(0)?];
22886        while matches!(self.peek(), Token::Comma) {
22887            self.advance();
22888            rhs.push(self.parse_expr(0)?);
22889        }
22890        if !matches!(self.peek(), Token::RParen) {
22891            return Err(self.err(alloc::format!(
22892                "expected ')' after right-hand row, got {:?}",
22893                self.peek()
22894            )));
22895        }
22896        self.advance();
22897        if rhs.len() != row.len() {
22898            // v7.39 (round 239) — PG's wording (42601).
22899            return Err(self.err("unequal number of entries in row expressions".to_string()));
22900        }
22901        Ok(match op {
22902            BinOp::Eq => row_eq(&row, &rhs),
22903            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22904            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22905            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22906            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22907            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22908            _ => unreachable!("op restricted above"),
22909        })
22910    }
22911
22912    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22913    /// escape character becomes the matcher's default backslash:
22914    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22915    /// → the char itself, and any pre-existing backslash escapes
22916    /// itself so it stays literal. Both operands must be string
22917    /// literals — a runtime pattern would need matcher support.
22918    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22919        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22920            (&pattern, &esc)
22921        else {
22922            return Err(
22923                "LIKE ... ESCAPE requires string-literal pattern and escape \
22924                 (runtime escape characters are not supported yet)"
22925                    .into(),
22926            );
22927        };
22928        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22929        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22930        // multi-character escape is an error.
22931        let esc_ch: Option<char> = {
22932            let mut ch_iter = e.chars();
22933            match (ch_iter.next(), ch_iter.next()) {
22934                (Some(c), None) => Some(c),
22935                (None, _) => None,
22936                (Some(_), Some(_)) => {
22937                    return Err(alloc::format!(
22938                        "ESCAPE must be a single character, got {e:?}"
22939                    ));
22940                }
22941            }
22942        };
22943        let mut out = String::with_capacity(p.len() + 4);
22944        let mut chars = p.chars();
22945        while let Some(c) = chars.next() {
22946            if Some(c) == esc_ch {
22947                match chars.next() {
22948                    // Escaped wildcard / escaped escape → keep the
22949                    // next char literal via backslash.
22950                    Some(next) => {
22951                        out.push('\\');
22952                        out.push(next);
22953                    }
22954                    None => {
22955                        return Err("LIKE pattern ends with the escape character".into());
22956                    }
22957                }
22958            } else if c == '\\' && esc_ch != Some('\\') {
22959                // A raw backslash is literal under a custom (or absent) escape
22960                // — escape it for the backslash-based matcher.
22961                out.push('\\');
22962                out.push('\\');
22963            } else {
22964                out.push(c);
22965            }
22966        }
22967        Ok(Expr::Literal(Literal::String(out)))
22968    }
22969
22970    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22971    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22972    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22973    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22974    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22975    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22976    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22977    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22978    /// array expression errors honestly rather than silently mismatching.
22979    fn try_like_any_all(
22980        &mut self,
22981        base: &Expr,
22982        negated: bool,
22983        case_insensitive: bool,
22984    ) -> Result<Option<Expr>, ParseError> {
22985        let is_any = match self.peek() {
22986            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22987            Token::Ident(s)
22988                if s.eq_ignore_ascii_case("any")
22989                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22990            {
22991                true
22992            }
22993            _ => return Ok(None),
22994        };
22995        self.advance(); // ANY / ALL
22996        self.advance(); // '('
22997        let arr = self.parse_expr(0)?;
22998        if !matches!(self.peek(), Token::RParen) {
22999            return Err(self.err(format!(
23000                "expected ')' after LIKE {} argument, got {:?}",
23001                if is_any { "ANY" } else { "ALL" },
23002                self.peek()
23003            )));
23004        }
23005        self.advance(); // ')'
23006        let Expr::Array(items) = arr else {
23007            return Err(self.err(
23008                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23009            ));
23010        };
23011        let mut clauses = items.into_iter().map(|p| Expr::Like {
23012            expr: Box::new(base.clone()),
23013            pattern: Box::new(p),
23014            negated,
23015            case_insensitive,
23016        });
23017        let Some(first) = clauses.next() else {
23018            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23019            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23020        };
23021        let op = if is_any { BinOp::Or } else { BinOp::And };
23022        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23023            lhs: Box::new(acc),
23024            op,
23025            rhs: Box::new(c),
23026        });
23027        Ok(Some(combined))
23028    }
23029
23030    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23031    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23032    /// `AND` is not swallowed.
23033    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23034        self.advance(); // BETWEEN
23035        // SYMMETRIC — the bounds may arrive in either order; both
23036        // orientations OR together. ASYMMETRIC is the default and
23037        // absorbs as noise.
23038        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23039        {
23040            self.advance();
23041            true
23042        } else {
23043            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23044                self.advance();
23045            }
23046            false
23047        };
23048        let low = self.parse_expr(6)?;
23049        if !matches!(self.peek(), Token::And) {
23050            return Err(self.err(format!(
23051                "expected AND after BETWEEN low bound, got {:?}",
23052                self.peek()
23053            )));
23054        }
23055        self.advance();
23056        let high = self.parse_expr(6)?;
23057        let target = Box::new(expr);
23058        let range = |lo: Expr, hi: Expr| Expr::Binary {
23059            lhs: Box::new(Expr::Binary {
23060                lhs: target.clone(),
23061                op: BinOp::GtEq,
23062                rhs: Box::new(lo),
23063            }),
23064            op: BinOp::And,
23065            rhs: Box::new(Expr::Binary {
23066                lhs: target.clone(),
23067                op: BinOp::LtEq,
23068                rhs: Box::new(hi),
23069            }),
23070        };
23071        let combined = if symmetric {
23072            Expr::Binary {
23073                lhs: Box::new(range(low.clone(), high.clone())),
23074                op: BinOp::Or,
23075                rhs: Box::new(range(high, low)),
23076            }
23077        } else {
23078            range(low, high)
23079        };
23080        Ok(maybe_not(combined, negated))
23081    }
23082
23083    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23084    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23085    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23086    /// Caller already consumed the leading `WITH` ident.
23087    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23088    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23089    /// self-reference that appears more than once in a single term.
23090    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23091        use crate::ast::{CteBody, SelectStatement};
23092        if !cte.recursive {
23093            return Ok(());
23094        }
23095        let CteBody::Select(body) = &cte.body else {
23096            return Ok(());
23097        };
23098        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23099        // check the anchor and every peer term.
23100        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23101        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23102        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23103            return Err(self.err(String::from(
23104                "ORDER BY in a recursive query is not implemented",
23105            )));
23106        }
23107        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23108            return Err(self.err(String::from(
23109                "LIMIT in a recursive query is not implemented",
23110            )));
23111        }
23112        let self_refs = |s: &SelectStatement| -> usize {
23113            let Some(from) = &s.from else {
23114                return 0;
23115            };
23116            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23117            for j in &from.joins {
23118                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23119                    n += 1;
23120                }
23121            }
23122            n
23123        };
23124        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23125            return Err(self.err(alloc::format!(
23126                "recursive reference to query \"{}\" must not appear more than once",
23127                cte.name
23128            )));
23129        }
23130        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23131        // apply only when the body actually references itself (a non-self-
23132        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23133        let anchor_refs = self_refs(body);
23134        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23135        if anchor_refs > 0 || union_refs {
23136            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23137            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23138            // "does not have the form" error — SPG used to compute a value.
23139            if body.unions.is_empty()
23140                || body.unions.iter().any(|(k, _)| {
23141                    !matches!(
23142                        k,
23143                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23144                    )
23145                })
23146            {
23147                return Err(self.err(alloc::format!(
23148                    "recursive query \"{}\" does not have the form non-recursive-term \
23149                     UNION [ALL] recursive-term",
23150                    cte.name
23151                )));
23152            }
23153            if anchor_refs > 0 {
23154                return Err(self.err(alloc::format!(
23155                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23156                    cte.name
23157                )));
23158            }
23159        }
23160        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23161        for (_, u) in &body.unions {
23162            if self_refs(u) == 0 {
23163                continue;
23164            }
23165            // The self-reference must not sit on the nullable side of an outer
23166            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23167            if let Some(from) = &u.from {
23168                for (i, j) in from.joins.iter().enumerate() {
23169                    let left_has_self = is_self(&from.primary)
23170                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23171                    let violated = match j.kind {
23172                        crate::ast::JoinKind::Left => is_self(&j.table),
23173                        crate::ast::JoinKind::Right => left_has_self,
23174                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23175                        _ => false,
23176                    };
23177                    if violated {
23178                        return Err(self.err(alloc::format!(
23179                            "recursive reference to query \"{}\" must not appear within an outer join",
23180                            cte.name
23181                        )));
23182                    }
23183                }
23184            }
23185            // No aggregates at the top level of the recursive term (SPG used
23186            // to run them and surface a misleading downstream error).
23187            let mut items_and_having: Vec<&Expr> = Vec::new();
23188            for it in &u.items {
23189                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23190                    items_and_having.push(expr);
23191                }
23192            }
23193            if let Some(h) = &u.having {
23194                items_and_having.push(h);
23195            }
23196            for e in items_and_having {
23197                if expr_has_toplevel_aggregate(e) {
23198                    return Err(self.err(String::from(
23199                        "aggregate functions are not allowed in a recursive query's recursive term",
23200                    )));
23201                }
23202            }
23203        }
23204        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23205        // subquery) anywhere in the body is rejected; a plain FROM derived
23206        // table is legal in PG and untouched here.
23207        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23208        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23209        for term in all_terms {
23210            if select_has_self_ref_in_sublink(term, &cte.name) {
23211                return Err(self.err(alloc::format!(
23212                    "recursive reference to query \"{}\" must not appear within a subquery",
23213                    cte.name
23214                )));
23215            }
23216        }
23217        Ok(())
23218    }
23219
23220    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23221    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23222    /// right after parse so the engine sees a plain recursive CTE with the
23223    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23224    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23225    /// text-rendered rows can't provide, and errors honestly.
23226    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23227        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23228        if cte.search.is_none() && cte.cycle.is_none() {
23229            return Ok(());
23230        }
23231        let cte_name = cte.name.clone();
23232        let col_names = cte.column_overrides.clone();
23233        if col_names.is_empty() {
23234            return Err(
23235                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23236            );
23237        }
23238        let search = cte.search.take();
23239        let cycle = cte.cycle.take();
23240        let mut extra_cols: Vec<String> = Vec::new();
23241        let col_ref = |name: &str| {
23242            Expr::Column(ColumnName {
23243                qualifier: Some(cte_name.clone()),
23244                name: name.to_string(),
23245            })
23246        };
23247        // Position of a SEARCH/CYCLE column within the CTE's column list.
23248        let pos_of = |name: &str| -> Result<usize, ParseError> {
23249            col_names
23250                .iter()
23251                .position(|c| c.eq_ignore_ascii_case(name))
23252                .ok_or_else(|| {
23253                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23254                })
23255        };
23256        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23257            let mut args = Vec::with_capacity(positions.len());
23258            for &p in positions {
23259                match items.get(p) {
23260                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23261                    _ => {
23262                        return Err(self.err(
23263                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23264                        ));
23265                    }
23266                }
23267            }
23268            Ok(Expr::FunctionCall {
23269                name: "row".into(),
23270                args,
23271            })
23272        };
23273        let CteBody::Select(body) = &mut cte.body else {
23274            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23275        };
23276        if body.unions.is_empty() {
23277            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23278        }
23279        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23280
23281        if let Some(srch) = search {
23282            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23283            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23284            // no typed `record[]`, but element-wise array ORDER BY is correct
23285            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23286            // exactly onto a typed array: DEPTH is the root→node path
23287            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23288            // orders numerically (multi-digit keys included), matching PG.
23289            //
23290            // A multi-column BY would need a record[] to keep the per-node key
23291            // tuple orderable, which SPG can't express — error honestly there
23292            // rather than mis-order.
23293            if srch.by_columns.len() != 1 {
23294                return Err(self.err(
23295                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23296                     SPG doesn't have yet; a single BY column is supported"
23297                        .into(),
23298                ));
23299            }
23300            let key_pos = pos_of(&srch.by_columns[0])?;
23301            let base_key = match body.items.get(key_pos) {
23302                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23303                _ => {
23304                    return Err(
23305                        self.err("SEARCH BY column maps to a non-expression select item".into())
23306                    );
23307                }
23308            };
23309            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23310                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23311                _ => {
23312                    return Err(
23313                        self.err("SEARCH BY column maps to a non-expression select item".into())
23314                    );
23315                }
23316            };
23317            if srch.depth_first {
23318                // base: ARRAY[key]; rec: array_append(cte.set, key).
23319                body.items.push(SelectItem::Expr {
23320                    expr: Expr::Array(alloc::vec![base_key]),
23321                    alias: Some(srch.set_column.clone()),
23322                });
23323                body.unions[rec].1.items.push(SelectItem::Expr {
23324                    expr: Expr::FunctionCall {
23325                        name: "array_append".into(),
23326                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23327                    },
23328                    alias: Some(srch.set_column.clone()),
23329                });
23330            } else {
23331                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23332                // leading depth element dominates the element-wise comparison,
23333                // so shallower rows sort first, then by key — PG's (depth, key).
23334                body.items.push(SelectItem::Expr {
23335                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23336                    alias: Some(srch.set_column.clone()),
23337                });
23338                // rec depth = cte.set[1] + 1.
23339                let parent_depth = Expr::ArraySubscript {
23340                    target: Box::new(col_ref(&srch.set_column)),
23341                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23342                };
23343                body.unions[rec].1.items.push(SelectItem::Expr {
23344                    expr: Expr::Array(alloc::vec![
23345                        Expr::Binary {
23346                            lhs: Box::new(parent_depth),
23347                            op: BinOp::Add,
23348                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23349                        },
23350                        rec_key,
23351                    ]),
23352                    alias: Some(srch.set_column.clone()),
23353                });
23354            }
23355            extra_cols.push(srch.set_column);
23356        }
23357
23358        if let Some(cyc) = cycle {
23359            let positions: Vec<usize> = cyc
23360                .columns
23361                .iter()
23362                .map(|c| pos_of(c))
23363                .collect::<Result<_, _>>()?;
23364            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23365            // cast it to text for the cycle path: membership only needs equality,
23366            // and the record text form gives SPG a TextArray path (SPG has no
23367            // typed record[] array). Cycle detection is unaffected.
23368            let base_row = Expr::Cast {
23369                expr: Box::new(row_of(&body.items, &positions)?),
23370                target: CastTarget::Text,
23371            };
23372            let rec_row = Expr::Cast {
23373                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23374                target: CastTarget::Text,
23375            };
23376            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23377            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23378            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23379            body.items.push(SelectItem::Expr {
23380                expr: Expr::Literal(dflt.clone()),
23381                alias: Some(cyc.mark_column.clone()),
23382            });
23383            body.items.push(SelectItem::Expr {
23384                expr: Expr::Array(alloc::vec![base_row]),
23385                alias: Some(cyc.path_column.clone()),
23386            });
23387            // rec mark: ROW(cols) already in the path → cycle.
23388            let hit = Expr::AnyAll {
23389                expr: Box::new(rec_row.clone()),
23390                op: BinOp::Eq,
23391                array: Box::new(col_ref(&cyc.path_column)),
23392                is_any: true,
23393            };
23394            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23395                Expr::Case {
23396                    operand: None,
23397                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23398                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23399                }
23400            } else {
23401                hit
23402            };
23403            body.unions[rec].1.items.push(SelectItem::Expr {
23404                expr: mark_expr,
23405                alias: Some(cyc.mark_column.clone()),
23406            });
23407            // rec path: array_append(cte.path, ROW(cols)).
23408            body.unions[rec].1.items.push(SelectItem::Expr {
23409                expr: Expr::FunctionCall {
23410                    name: "array_append".into(),
23411                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23412                },
23413                alias: Some(cyc.path_column.clone()),
23414            });
23415            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23416            let stop = Expr::Unary {
23417                op: UnOp::Not,
23418                expr: Box::new(col_ref(&cyc.mark_column)),
23419            };
23420            let w = &mut body.unions[rec].1.where_;
23421            *w = Some(match w.take() {
23422                Some(prev) => Expr::Binary {
23423                    lhs: Box::new(prev),
23424                    op: BinOp::And,
23425                    rhs: Box::new(stop),
23426                },
23427                None => stop,
23428            });
23429            extra_cols.push(cyc.mark_column);
23430            extra_cols.push(cyc.path_column);
23431        }
23432        cte.column_overrides.extend(extra_cols);
23433        Ok(())
23434    }
23435
23436    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23437    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23438    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23439        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23440            return Ok(None);
23441        }
23442        self.advance(); // SEARCH
23443        let depth_first = match self.peek() {
23444            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23445            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23446            other => {
23447                return Err(self.err(format!(
23448                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23449                )));
23450            }
23451        };
23452        self.advance();
23453        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23454            return Err(self.err(format!(
23455                "expected FIRST after SEARCH mode, got {:?}",
23456                self.peek()
23457            )));
23458        }
23459        self.advance();
23460        if !self.peek_is_by() {
23461            return Err(self.err(format!(
23462                "expected BY after SEARCH … FIRST, got {:?}",
23463                self.peek()
23464            )));
23465        }
23466        self.advance();
23467        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23468        while matches!(self.peek(), Token::Comma) {
23469            self.advance();
23470            by_columns.push(self.expect_ident_like()?);
23471        }
23472        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23473            return Err(self.err(format!(
23474                "expected SET in SEARCH clause, got {:?}",
23475                self.peek()
23476            )));
23477        }
23478        self.advance();
23479        let set_column = self.expect_ident_like()?;
23480        Ok(Some(crate::ast::SearchClause {
23481            depth_first,
23482            by_columns,
23483            set_column,
23484        }))
23485    }
23486
23487    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23488    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23489    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23490        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23491            return Ok(None);
23492        }
23493        self.advance(); // CYCLE
23494        let mut columns = alloc::vec![self.expect_ident_like()?];
23495        while matches!(self.peek(), Token::Comma) {
23496            self.advance();
23497            columns.push(self.expect_ident_like()?);
23498        }
23499        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23500            return Err(self.err(format!(
23501                "expected SET in CYCLE clause, got {:?}",
23502                self.peek()
23503            )));
23504        }
23505        self.advance();
23506        let mark_column = self.expect_ident_like()?;
23507        let mut mark_value = None;
23508        let mut default_value = None;
23509        if matches!(self.peek(), Token::To) {
23510            self.advance();
23511            mark_value = Some(self.parse_cycle_literal()?);
23512            if !matches!(self.peek(), Token::Default) {
23513                return Err(self.err(format!(
23514                    "expected DEFAULT after CYCLE … TO, got {:?}",
23515                    self.peek()
23516                )));
23517            }
23518            self.advance();
23519            default_value = Some(self.parse_cycle_literal()?);
23520        }
23521        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23522            return Err(self.err(format!(
23523                "expected USING in CYCLE clause, got {:?}",
23524                self.peek()
23525            )));
23526        }
23527        self.advance();
23528        let path_column = self.expect_ident_like()?;
23529        Ok(Some(crate::ast::CycleClause {
23530            columns,
23531            mark_column,
23532            mark_value,
23533            default_value,
23534            path_column,
23535        }))
23536    }
23537
23538    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23539    /// literal (string / bool / number) in PG.
23540    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23541        match self.parse_expr(0)? {
23542            Expr::Literal(l) => Ok(l),
23543            other => Err(self.err(format!(
23544                "CYCLE mark/default value must be a literal, got {other:?}"
23545            ))),
23546        }
23547    }
23548
23549    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23550        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23551        // Comes through as an identifier; consume it if present and
23552        // mark every CTE in the clause as recursive (PG semantics —
23553        // the flag is per-WITH, not per-CTE).
23554        let mut recursive = false;
23555        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23556            && s.eq_ignore_ascii_case("recursive")
23557        {
23558            self.advance();
23559            recursive = true;
23560        }
23561        let mut ctes = Vec::new();
23562        loop {
23563            let name = self.expect_ident_like()?;
23564            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23565            // PG uses these to rename the body's output columns; we
23566            // do the same below by overriding `columns[i].name`.
23567            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23568                self.advance();
23569                let mut names = Vec::new();
23570                loop {
23571                    names.push(self.expect_ident_like()?);
23572                    if matches!(self.peek(), Token::Comma) {
23573                        self.advance();
23574                        continue;
23575                    }
23576                    break;
23577                }
23578                if !matches!(self.peek(), Token::RParen) {
23579                    return Err(self.err(format!(
23580                        "expected ')' to close CTE column list, got {:?}",
23581                        self.peek()
23582                    )));
23583                }
23584                self.advance();
23585                names
23586            } else {
23587                Vec::new()
23588            };
23589            // AS is a reserved Token::As (used by SELECT-item / FROM
23590            // aliasing) — handle it specially rather than as a bare
23591            // ident.
23592            if !matches!(self.peek(), Token::As) {
23593                return Err(self.err(format!(
23594                    "expected AS after CTE name {name:?}, got {:?}",
23595                    self.peek()
23596                )));
23597            }
23598            self.advance();
23599            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23600            // MATERIALIZED` optimizer hints. SPG materialises every
23601            // CTE, so both spellings are accepted and absorbed.
23602            if matches!(self.peek(), Token::Not) {
23603                self.advance(); // NOT
23604                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23605                    if s.eq_ignore_ascii_case("materialized"))
23606                {
23607                    self.advance();
23608                } else {
23609                    return Err(self.err(format!(
23610                        "expected MATERIALIZED after AS NOT, got {:?}",
23611                        self.peek()
23612                    )));
23613                }
23614            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23615                if s.eq_ignore_ascii_case("materialized"))
23616            {
23617                self.advance();
23618            }
23619            if !matches!(self.peek(), Token::LParen) {
23620                return Err(self.err(format!(
23621                    "expected '(' after AS in WITH clause, got {:?}",
23622                    self.peek()
23623                )));
23624            }
23625            self.advance();
23626            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23627            // RETURNING) as the CTE body in addition to SELECT.
23628            // PG writable CTE semantics. UPDATE / DELETE come in as
23629            // bare Idents (lexer keeps SELECT / INSERT as reserved
23630            // tokens but treats the rest of DML as case-insensitive
23631            // idents).
23632            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23633            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23634            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23635            let body = match self.peek() {
23636                Token::Select => {
23637                    let inner = self.parse_select_stmt()?;
23638                    let Statement::Select(s) = inner else {
23639                        unreachable!("parse_select_stmt returns Select");
23640                    };
23641                    crate::ast::CteBody::Select(s)
23642                }
23643                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23644                // `SELECT * FROM t` this way and accepts it wherever a
23645                // SELECT goes, so the CTE body dispatch needs its own
23646                // arm: this match is keyed on the FIRST token, and
23647                // `Token::Table` fell through to a tail that then
23648                // rejected what it got. `parse_table_shorthand` has
23649                // returned a desugared SelectStatement since the
23650                // shorthand landed — only the routing was missing.
23651                // Round 868 found this by putting the shorthand in a
23652                // subquery; every earlier check used a top-level form.
23653                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23654                // `SELECT * FROM t` this way and accepts it wherever a
23655                // SELECT goes, so the CTE body dispatch needs its own
23656                // arm: this match is keyed on the FIRST token, and
23657                // `Token::Table` fell through to a tail that rejected
23658                // what it got. `parse_table_shorthand` has returned a
23659                // desugared SelectStatement since the shorthand landed —
23660                // only the routing was missing, here and in the derived
23661                // table's second-token gate. Round 868 found both by
23662                // putting the shorthand in a subquery; every earlier
23663                // check had used a top-level form.
23664                Token::Table
23665                    if matches!(
23666                        self.tokens.get(self.pos + 1),
23667                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23668                    ) =>
23669                {
23670                    let mut head = self.parse_table_shorthand()?;
23671                    self.parse_setop_chain_into(&mut head)?;
23672                    self.parse_select_tail_into(&mut head)?;
23673                    crate::ast::CteBody::Select(head)
23674                }
23675                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23676                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23677                // the shared rows helper onto a Select body.
23678                Token::Values => {
23679                    self.advance(); // VALUES
23680                    let mut head = self.parse_values_rows_body()?;
23681                    // A VALUES seed can head a set-operation chain —
23682                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23683                    // SELECT n+1 FROM t …). Attach any trailing
23684                    // UNION / INTERSECT / EXCEPT peers so the
23685                    // recursive-CTE body parses like the SELECT seed.
23686                    self.parse_setop_chain_into(&mut head)?;
23687                    crate::ast::CteBody::Select(head)
23688                }
23689                Token::Insert => {
23690                    let inner = self.parse_one_statement()?;
23691                    let Statement::Insert(s) = inner else {
23692                        unreachable!("Token::Insert routes to Insert");
23693                    };
23694                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23695                }
23696                _ if is_update_kw => {
23697                    let inner = self.parse_one_statement()?;
23698                    let Statement::Update(s) = inner else {
23699                        return Err(
23700                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23701                        );
23702                    };
23703                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23704                }
23705                _ if is_delete_kw => {
23706                    let inner = self.parse_one_statement()?;
23707                    let Statement::Delete(s) = inner else {
23708                        return Err(
23709                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23710                        );
23711                    };
23712                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23713                }
23714                // v7.39 (round 149) — PG 17 allows MERGE as a
23715                // data-modifying CTE body.
23716                _ if is_merge_kw => {
23717                    let inner = self.parse_one_statement()?;
23718                    let Statement::Merge(s) = inner else {
23719                        return Err(
23720                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23721                        );
23722                    };
23723                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23724                }
23725                // v7.39 (round 151) — a CTE body may itself be
23726                // WITH-headed (PG grammar: PreparableStmt carries its
23727                // own with_clause). The nested statement keeps its own
23728                // ctes; the modifying-CTE-at-top-level rule is enforced
23729                // at execution.
23730                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23731                    self.advance(); // WITH
23732                    match self.parse_with_cte_then_select()? {
23733                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23734                        Statement::Insert(s) => {
23735                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23736                        }
23737                        Statement::Update(s) => {
23738                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23739                        }
23740                        Statement::Delete(s) => {
23741                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23742                        }
23743                        Statement::Merge(s) => {
23744                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23745                        }
23746
23747                        other => {
23748                            return Err(self.err(format!(
23749                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23750                            )));
23751                        }
23752                    }
23753                }
23754                other => {
23755                    return Err(self.err(format!(
23756                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23757                    )));
23758                }
23759            };
23760            if !matches!(self.peek(), Token::RParen) {
23761                return Err(self.err(format!(
23762                    "expected ')' after CTE body, got {:?}",
23763                    self.peek()
23764                )));
23765            }
23766            self.advance();
23767            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23768            // CTE, desugared into extra body columns by the engine.
23769            let search = self.parse_cte_search_clause()?;
23770            let cycle = self.parse_cte_cycle_clause()?;
23771            let mut cte = crate::ast::Cte {
23772                name,
23773                body,
23774                recursive,
23775                column_overrides,
23776                search,
23777                cycle,
23778            };
23779            self.validate_recursive_cte(&cte)?;
23780            self.desugar_cte_search_cycle(&mut cte)?;
23781            ctes.push(cte);
23782            if matches!(self.peek(), Token::Comma) {
23783                self.advance();
23784                continue;
23785            }
23786            break;
23787        }
23788        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23789        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23790        // the parsed CTEs to whichever statement the body produces.
23791        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23792        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23793        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23794        match self.peek() {
23795            Token::Select => {
23796                let body_stmt = self.parse_select_stmt()?;
23797                let Statement::Select(mut body) = body_stmt else {
23798                    unreachable!()
23799                };
23800                body.ctes = ctes;
23801                Ok(Statement::Select(body))
23802            }
23803            Token::Insert => {
23804                let body_stmt = self.parse_one_statement()?;
23805                let Statement::Insert(mut body) = body_stmt else {
23806                    unreachable!()
23807                };
23808                body.ctes = ctes;
23809                Ok(Statement::Insert(body))
23810            }
23811            _ if outer_is_update => {
23812                let body_stmt = self.parse_one_statement()?;
23813                let Statement::Update(mut body) = body_stmt else {
23814                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23815                };
23816                body.ctes = ctes;
23817                Ok(Statement::Update(body))
23818            }
23819            _ if outer_is_delete => {
23820                let body_stmt = self.parse_one_statement()?;
23821                let Statement::Delete(mut body) = body_stmt else {
23822                    return Err(self.err(format!("expected DELETE after WITH clause")));
23823                };
23824                body.ctes = ctes;
23825                Ok(Statement::Delete(body))
23826            }
23827            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23828            // WITH RECURSIVE is rejected with PG's exact message
23829            // (parse analysis, transformWithClause).
23830            _ if outer_is_merge => {
23831                if recursive {
23832                    return Err(self.err(String::from(
23833                        "WITH RECURSIVE is not supported for MERGE statement",
23834                    )));
23835                }
23836                let body_stmt = self.parse_one_statement()?;
23837                let Statement::Merge(mut body) = body_stmt else {
23838                    return Err(self.err(format!("expected MERGE after WITH clause")));
23839                };
23840                body.ctes = ctes;
23841                Ok(Statement::Merge(body))
23842            }
23843            other => Err(self.err(format!(
23844                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23845            ))),
23846        }
23847    }
23848
23849    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23850    /// already consumed the leading `EXISTS` ident via
23851    /// `self.advance()`.
23852    /// v7.13.0 — parse the rest of a `CASE … END` expression after
23853    /// the leading `CASE` ident has been consumed (mailrs round-5
23854    /// G9). Supports both the searched form
23855    /// (`CASE WHEN cond THEN val …`) and the simple form
23856    /// (`CASE operand WHEN val THEN val …`).
23857    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23858        // Disambiguate searched vs simple form: if the next token
23859        // is `WHEN`, we're in the searched form. Otherwise the
23860        // intervening expression is the operand.
23861        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23862            None
23863        } else {
23864            Some(Box::new(self.parse_expr(0)?))
23865        };
23866        let mut branches: Vec<(Expr, Expr)> = Vec::new();
23867        loop {
23868            match self.peek() {
23869                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23870                    self.advance();
23871                    let cond = self.parse_expr(0)?;
23872                    match self.peek() {
23873                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23874                            self.advance();
23875                        }
23876                        other => {
23877                            return Err(self.err(alloc::format!(
23878                                "expected THEN after CASE WHEN <expr>, got {other:?}"
23879                            )));
23880                        }
23881                    }
23882                    let value = self.parse_expr(0)?;
23883                    branches.push((cond, value));
23884                }
23885                _ => break,
23886            }
23887        }
23888        if branches.is_empty() {
23889            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23890        }
23891        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23892        {
23893            self.advance();
23894            Some(Box::new(self.parse_expr(0)?))
23895        } else {
23896            None
23897        };
23898        match self.peek() {
23899            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23900                self.advance();
23901            }
23902            other => {
23903                return Err(self.err(alloc::format!(
23904                    "expected END to close CASE expression, got {other:?}"
23905                )));
23906            }
23907        }
23908        Ok(Expr::Case {
23909            operand,
23910            branches,
23911            else_branch,
23912        })
23913    }
23914
23915    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23916    /// query-source position (EXISTS / IN / INSERT source / CTE body /
23917    /// view body). Caller consumed the WITH keyword. Only a SELECT
23918    /// outer is grammatical here; the data-modifying-CTE-at-top-level
23919    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23920    /// maps correctly.
23921    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23922        let inner = self.parse_with_cte_then_select()?;
23923        match inner {
23924            Statement::Select(s) => Ok(s),
23925            other => Err(self.err(format!(
23926                "expected SELECT after WITH in a subquery, got {other:?}"
23927            ))),
23928        }
23929    }
23930
23931    /// True when the next token is the (unquoted) WITH keyword. WITH is
23932    /// reserved in PG, so a bare `with` can never be a column reference
23933    /// in these positions; a quoted `"with"` stays an identifier.
23934    fn peek_is_with_kw(&self) -> bool {
23935        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23936    }
23937
23938    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23939    /// `#[inline(never)]` keeps the large SelectStatement temporaries
23940    /// off parse_expr's recursive frame (the nesting-budget stack
23941    /// cliff — see the round-153 gate regression).
23942    #[inline(never)]
23943    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23944        if self.peek_is_with_kw() {
23945            self.advance();
23946            self.parse_nested_with_select()
23947        } else {
23948            match self.parse_select_stmt()? {
23949                Statement::Select(s) => Ok(s),
23950                other => Err(self.err(alloc::format!(
23951                    "expected SELECT inside ANY/ALL, got {other:?}"
23952                ))),
23953            }
23954        }
23955    }
23956
23957    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23958        if !matches!(self.peek(), Token::LParen) {
23959            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23960        }
23961        self.advance();
23962        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23963        let s = if self.peek_is_with_kw() {
23964            self.advance();
23965            self.parse_nested_with_select()?
23966        } else {
23967            let inner = self.parse_select_stmt()?;
23968            let Statement::Select(s) = inner else {
23969                unreachable!("parse_select_stmt returns Select")
23970            };
23971            s
23972        };
23973        if !matches!(self.peek(), Token::RParen) {
23974            return Err(self.err(format!(
23975                "expected ')' after EXISTS-subquery, got {:?}",
23976                self.peek()
23977            )));
23978        }
23979        self.advance();
23980        Ok(Expr::Exists {
23981            subquery: Box::new(s),
23982            negated,
23983        })
23984    }
23985
23986    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23987        self.advance(); // IN
23988        if !matches!(self.peek(), Token::LParen) {
23989            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23990        }
23991        self.advance();
23992        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23993        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23994        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23995            let s = if self.peek_is_with_kw() {
23996                self.advance();
23997                self.parse_nested_with_select()?
23998            } else {
23999                let inner = self.parse_select_stmt()?;
24000                let Statement::Select(s) = inner else {
24001                    unreachable!("parse_select_stmt always returns Statement::Select")
24002                };
24003                s
24004            };
24005            if !matches!(self.peek(), Token::RParen) {
24006                return Err(self.err(format!(
24007                    "expected ')' after IN-subquery, got {:?}",
24008                    self.peek()
24009                )));
24010            }
24011            self.advance();
24012            return Ok(Expr::InSubquery {
24013                expr: Box::new(expr),
24014                subquery: Box::new(s),
24015                negated,
24016            });
24017        }
24018        let mut elements = Vec::new();
24019        if !matches!(self.peek(), Token::RParen) {
24020            loop {
24021                elements.push(self.parse_expr(0)?);
24022                match self.peek() {
24023                    Token::Comma => {
24024                        self.advance();
24025                    }
24026                    Token::RParen => break,
24027                    other => {
24028                        return Err(
24029                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24030                        );
24031                    }
24032                }
24033            }
24034        }
24035        self.advance(); // ')'
24036        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24037        // left-deep OR-Eq chain: chain depth scaled with the element
24038        // count and overflowed the stack (eval + drop are recursive).
24039        if elements.is_empty() {
24040            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24041        }
24042        Ok(Expr::InList {
24043            expr: Box::new(expr),
24044            list: elements,
24045            negated,
24046        })
24047    }
24048
24049    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24050    /// already consumed by the caller. Elements must be numeric literals
24051    /// (with optional unary `-`); any compound expression is rejected at
24052    /// parse time so the runtime never needs to evaluate inside a vector.
24053    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24054    /// has already consumed the `EXTRACT` token before calling us —
24055    /// we pick up at the opening `(`.
24056    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24057    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24058    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24059    /// per-column OR-fold of
24060    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24061    /// term)` so the existing FTS evaluator handles semantics.
24062    ///
24063    /// The mode modifier is accepted-and-ignored at v7.17 — all
24064    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24065    /// mode operators (`+foo -bar`) would need their own parser
24066    /// (Phase 2.2c); customers who hit them today already get a
24067    /// correct lexeme-match against the bare term, only without
24068    /// the +/- precedence the customer asked for.
24069    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24070        // Already at `MATCH`-consumed position; the dispatcher
24071        // confirmed the next token is `(`.
24072        if !matches!(self.peek(), Token::LParen) {
24073            return Err(self.err(alloc::format!(
24074                "expected '(' after MATCH, got {:?}",
24075                self.peek()
24076            )));
24077        }
24078        self.advance();
24079        let mut cols: Vec<Expr> = Vec::new();
24080        loop {
24081            cols.push(self.parse_expr(0)?);
24082            match self.peek() {
24083                Token::Comma => {
24084                    self.advance();
24085                }
24086                Token::RParen => break,
24087                other => {
24088                    return Err(self.err(alloc::format!(
24089                        "expected ',' or ')' in MATCH column list, got {other:?}"
24090                    )));
24091                }
24092            }
24093        }
24094        self.advance(); // ')'
24095        // Expect AGAINST.
24096        match self.peek() {
24097            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24098                self.advance();
24099            }
24100            other => {
24101                return Err(self.err(alloc::format!(
24102                    "expected AGAINST after MATCH column list, got {other:?}"
24103                )));
24104            }
24105        }
24106        if !matches!(self.peek(), Token::LParen) {
24107            return Err(self.err(alloc::format!(
24108                "expected '(' after AGAINST, got {:?}",
24109                self.peek()
24110            )));
24111        }
24112        self.advance();
24113        // Read AGAINST's argument as a single primary token —
24114        // string literal, placeholder, or column-ref ident. We
24115        // can't call `parse_expr` / `parse_unary` here because
24116        // the postfix chain inside `parse_atom` would greedily
24117        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24118        // and fail at "expected '(' after IN". Customers always
24119        // write a literal or bound parameter in AGAINST, so this
24120        // restriction is non-blocking; the error path explains
24121        // the limit if a more complex expression shows up.
24122        let term = match self.advance() {
24123            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24124            Token::Placeholder(n) => Expr::Placeholder(n),
24125            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24126                qualifier: None,
24127                name: s,
24128            }),
24129            other => {
24130                return Err(self.err(alloc::format!(
24131                    "MATCH ... AGAINST(<term>) expects a string literal, \
24132                     bound parameter, or column ref, got {other:?}"
24133                )));
24134            }
24135        };
24136        // Optional mode tail — accept-and-ignore at v7.17:
24137        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24138        //   IN BOOLEAN MODE
24139        //   WITH QUERY EXPANSION
24140        loop {
24141            match self.peek() {
24142                // IN lexes as a reserved Token::In, not an ident,
24143                // so it gets its own arm.
24144                Token::In => {
24145                    self.advance();
24146                }
24147                Token::Ident(s) | Token::QuotedIdent(s)
24148                    if s.eq_ignore_ascii_case("natural")
24149                        || s.eq_ignore_ascii_case("language")
24150                        || s.eq_ignore_ascii_case("boolean")
24151                        || s.eq_ignore_ascii_case("mode")
24152                        || s.eq_ignore_ascii_case("with")
24153                        || s.eq_ignore_ascii_case("query")
24154                        || s.eq_ignore_ascii_case("expansion") =>
24155                {
24156                    self.advance();
24157                }
24158                _ => break,
24159            }
24160        }
24161        if !matches!(self.peek(), Token::RParen) {
24162            return Err(self.err(alloc::format!(
24163                "expected ')' to close AGAINST, got {:?}",
24164                self.peek()
24165            )));
24166        }
24167        self.advance();
24168        // Build per-column `to_tsvector('simple', col) @@
24169        // plainto_tsquery('simple', term)` and OR-fold.
24170        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24171        let plainto = Expr::FunctionCall {
24172            name: String::from("plainto_tsquery"),
24173            args: alloc::vec![simple_lit(), term.clone()],
24174        };
24175        let mut folded: Option<Expr> = None;
24176        for col in cols {
24177            let to_tsv = Expr::FunctionCall {
24178                name: String::from("to_tsvector"),
24179                args: alloc::vec![simple_lit(), col],
24180            };
24181            let leaf = Expr::Binary {
24182                lhs: Box::new(to_tsv),
24183                op: crate::ast::BinOp::TsMatch,
24184                rhs: Box::new(plainto.clone()),
24185            };
24186            folded = Some(match folded {
24187                None => leaf,
24188                Some(prev) => Expr::Binary {
24189                    lhs: Box::new(prev),
24190                    op: crate::ast::BinOp::Or,
24191                    rhs: Box::new(leaf),
24192                },
24193            });
24194        }
24195        match folded {
24196            Some(e) => Ok(e),
24197            None => Err(self.err(String::from(
24198                "MATCH(...) AGAINST(...) requires at least one column",
24199            ))),
24200        }
24201    }
24202
24203    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24204        if !matches!(self.peek(), Token::LParen) {
24205            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24206        }
24207        self.advance();
24208        let field_name = self.expect_ident_like()?;
24209        let field = match field_name.to_ascii_lowercase().as_str() {
24210            // PG accepts the plural spellings (years/months/…/millenniums) as
24211            // aliases for the singular fields — its datetime unit table has both.
24212            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24213            "year" | "years" => ExtractField::Year,
24214            "month" | "months" => ExtractField::Month,
24215            "day" | "days" => ExtractField::Day,
24216            "hour" | "hours" => ExtractField::Hour,
24217            "minute" | "minutes" => ExtractField::Minute,
24218            "second" | "seconds" => ExtractField::Second,
24219            "microsecond" | "microseconds" => ExtractField::Microsecond,
24220            "epoch" => ExtractField::Epoch,
24221            "dow" => ExtractField::Dow,
24222            "isodow" => ExtractField::Isodow,
24223            "doy" => ExtractField::Doy,
24224            "week" | "weeks" => ExtractField::Week,
24225            "isoyear" => ExtractField::Isoyear,
24226            "quarter" => ExtractField::Quarter,
24227            "decade" | "decades" => ExtractField::Decade,
24228            "century" | "centuries" => ExtractField::Century,
24229            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24230            "julian" => ExtractField::Julian,
24231            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24232            "timezone" => ExtractField::Timezone,
24233            "timezone_hour" => ExtractField::TimezoneHour,
24234            "timezone_minute" => ExtractField::TimezoneMinute,
24235            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24236            // reports an unknown one with the source type (22023); carry the
24237            // raw name so eval can word it.
24238            other => ExtractField::Other(alloc::string::String::from(other)),
24239        };
24240        if !matches!(self.peek(), Token::From) {
24241            return Err(self.err(format!(
24242                "expected FROM after EXTRACT field, got {:?}",
24243                self.peek()
24244            )));
24245        }
24246        self.advance();
24247        let source = self.parse_expr(0)?;
24248        if !matches!(self.peek(), Token::RParen) {
24249            return Err(self.err(format!(
24250                "expected ')' to close EXTRACT, got {:?}",
24251                self.peek()
24252            )));
24253        }
24254        self.advance();
24255        Ok(Expr::Extract {
24256            field,
24257            source: Box::new(source),
24258        })
24259    }
24260
24261    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24262    /// is already consumed; we expect a single string literal next and
24263    /// resolve it into `Literal::Interval` at parse time so the engine
24264    /// never has to re-tokenise inside the string.
24265    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24266    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24267    /// is the SQL-standard form and is left to the path below.
24268    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24269        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24270        let (offset, sign) = match self.peek() {
24271            Token::Minus => (1, "-"),
24272            _ => (0, ""),
24273        };
24274        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24275            return None;
24276        };
24277        self.tokens
24278            .get(self.pos + offset + 1)
24279            .filter(|t| mysql_interval_unit(t).is_some())?;
24280        Some((alloc::format!("{sign}{n}"), offset + 1))
24281    }
24282
24283    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24284    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24285    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24286    ///
24287    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24288    /// this by parsing the group and then restoring `self.pos` — which could
24289    /// never have worked, because `advance()` DESTROYS the token it returns
24290    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24291    /// inert only because both branches errored back then.
24292    fn interval_paren_is_quantity(&self) -> bool {
24293        let mut depth = 0usize;
24294        let mut saw_top_level_comma = false;
24295        let mut i = self.pos;
24296        while let Some(tok) = self.tokens.get(i) {
24297            match tok {
24298                Token::LParen => depth += 1,
24299                Token::RParen => {
24300                    depth = depth.saturating_sub(1);
24301                    if depth == 0 {
24302                        return !saw_top_level_comma
24303                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24304                                .is_some();
24305                    }
24306                }
24307                // A comma directly inside the outermost parens means the
24308                // argument list of the INTERVAL() function.
24309                Token::Comma if depth == 1 => saw_top_level_comma = true,
24310                Token::Eof => return false,
24311                _ => {}
24312            }
24313            i += 1;
24314        }
24315        false
24316    }
24317
24318    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24319        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24320        // (the index of the last Ni ≤ N), distinct from the interval literal.
24321        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24322        // is decided by a non-destructive lookahead (round 422) before either
24323        // branch consumes anything. MySQL only.
24324        if self.mysql_dialect
24325            && matches!(self.peek(), Token::LParen)
24326            && !self.interval_paren_is_quantity()
24327        {
24328            self.advance(); // (
24329            let mut args = Vec::new();
24330            if !matches!(self.peek(), Token::RParen) {
24331                loop {
24332                    args.push(self.parse_expr(0)?);
24333                    if matches!(self.peek(), Token::Comma) {
24334                        self.advance();
24335                        continue;
24336                    }
24337                    break;
24338                }
24339            }
24340            if !matches!(self.peek(), Token::RParen) {
24341                return Err(self.err(alloc::format!(
24342                    "expected ')' after INTERVAL() arguments, got {:?}",
24343                    self.peek()
24344                )));
24345            }
24346            self.advance(); // )
24347            return Ok(Expr::FunctionCall {
24348                name: alloc::string::String::from("interval"),
24349                args,
24350            });
24351        }
24352        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24353        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24354        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24355        // writes every date arithmetic there is, and it did not parse at
24356        // all. PG rejects the unquoted form outright (`syntax error at or
24357        // near "1"`, measured), so it is taken only in the MySQL dialect —
24358        // PG's own `INTERVAL '1' DAY` is untouched below.
24359        if self.mysql_dialect
24360            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24361        {
24362            for _ in 0..consume {
24363                self.advance(); // the optional `-` and the number
24364            }
24365            let Some(unit) = mysql_interval_unit(self.peek()) else {
24366                return Err(self.err(alloc::format!(
24367                    "expected an interval unit after INTERVAL {text}, got {:?}",
24368                    self.peek()
24369                )));
24370            };
24371            self.advance(); // the unit
24372            let (months, days, micros) = scale_mysql_interval(&text, unit)
24373                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24374            return Ok(Expr::Literal(Literal::Interval {
24375                months,
24376                days,
24377                micros,
24378                // The canonical rendering, so Display round-trips into a
24379                // form both dialects read back.
24380                text: alloc::format!("{text} {unit}"),
24381            }));
24382        }
24383        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24384        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24385        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24386        // Those cannot fold into a compile-time `Literal::Interval`, so they
24387        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24388        // builtin, which builds the value at run time (and yields NULL for a
24389        // NULL quantity, as MariaDB does). The literal path above still folds
24390        // the constant case — it is cheaper and round-trips through Display.
24391        //
24392        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24393        // MySQL's quoted spelling) keep the qualifier path below.
24394        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24395            let qty = self.parse_expr(0)?;
24396            let Some(unit) = mysql_interval_unit(self.peek()) else {
24397                return Err(self.err(alloc::format!(
24398                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24399                    self.peek()
24400                )));
24401            };
24402            self.advance(); // the unit
24403            return Ok(make_interval_call(qty, unit));
24404        }
24405        let tok = self.advance();
24406        let Token::String(text) = tok else {
24407            return Err(self.err(format!(
24408                "expected string literal after INTERVAL, got {tok:?}"
24409            )));
24410        };
24411        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24412        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24413        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24414        // bare number means and the leading/trailing precision.
24415        let field1 = interval_field_of(self.peek());
24416        let qualifier = if let Some(f1) = field1 {
24417            self.advance();
24418            let f2 = if matches!(self.peek(), Token::To) {
24419                self.advance();
24420                let Some(f) = interval_field_of(self.peek()) else {
24421                    return Err(self.err(format!(
24422                        "expected an interval field after TO, got {:?}",
24423                        self.peek()
24424                    )));
24425                };
24426                self.advance();
24427                Some(f)
24428            } else {
24429                None
24430            };
24431            Some((f1, f2))
24432        } else {
24433            None
24434        };
24435        let (months, days, micros) = match qualifier {
24436            Some(q) => interpret_qualified_interval(&text, q),
24437            None => parse_interval_text(&text),
24438        }
24439        .ok_or_else(|| ParseError {
24440            message: format!(
24441                "cannot parse INTERVAL {text:?}; \
24442                     expected `<n> <unit> [<n> <unit> ...]` with units \
24443                     microsecond[s], millisecond[s], second[s], minute[s], \
24444                     hour[s], day[s], week[s], month[s], year[s]"
24445            ),
24446            token_pos: self.consumed_pos(),
24447        })?;
24448        Ok(Expr::Literal(Literal::Interval {
24449            months,
24450            days,
24451            micros,
24452            text,
24453        }))
24454    }
24455
24456    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24457    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24458    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24459    /// than a pgvector literal.
24460    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24461        self.advance(); // consume `[`
24462        let mut items: Vec<Expr> = Vec::new();
24463        if !matches!(self.peek(), Token::RBracket) {
24464            loop {
24465                if matches!(self.peek(), Token::LBracket) {
24466                    items.push(self.parse_array_bracket_body()?);
24467                } else {
24468                    items.push(self.parse_expr(0)?);
24469                }
24470                match self.peek() {
24471                    Token::Comma => {
24472                        self.advance();
24473                    }
24474                    Token::RBracket => break,
24475                    other => {
24476                        return Err(self.err(alloc::format!(
24477                            "expected ',' or ']' in array literal, got {other:?}"
24478                        )));
24479                    }
24480                }
24481            }
24482        }
24483        self.advance(); // consume `]`
24484        Ok(Expr::Array(items))
24485    }
24486
24487    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24488        let mut elems = Vec::new();
24489        if matches!(self.peek(), Token::RBracket) {
24490            self.advance();
24491            return Ok(Expr::Literal(Literal::Vector(elems)));
24492        }
24493        loop {
24494            let e = self.parse_expr(0)?;
24495            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24496                message: format!("vector element must be a numeric literal, got {e:?}"),
24497                token_pos: self.pos,
24498            })?;
24499            elems.push(x);
24500            match self.peek() {
24501                Token::Comma => {
24502                    self.advance();
24503                }
24504                Token::RBracket => {
24505                    self.advance();
24506                    break;
24507                }
24508                other => {
24509                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24510                }
24511            }
24512        }
24513        Ok(Expr::Literal(Literal::Vector(elems)))
24514    }
24515
24516    /// Atom that started with an identifier: could be `t.col`, `col`, or
24517    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24518    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24519    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24520    /// is optional; an empty `()` is also legal (PG semantics).
24521    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24522    /// modifier between `name(args)` and `OVER (...)`. Default is
24523    /// `Respect`. Unrecognised idents leave the stream unchanged.
24524    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24525        let Token::Ident(s) = self.peek().clone() else {
24526            return NullTreatment::Respect;
24527        };
24528        let is_ignore = s.eq_ignore_ascii_case("ignore");
24529        let is_respect = s.eq_ignore_ascii_case("respect");
24530        if !is_ignore && !is_respect {
24531            return NullTreatment::Respect;
24532        }
24533        // Lookahead for NULLS — only consume both tokens together.
24534        // pos+1 must hold a "nulls" ident.
24535        if self.pos + 1 < self.tokens.len()
24536            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24537            && s2.eq_ignore_ascii_case("nulls")
24538        {
24539            self.advance();
24540            self.advance();
24541            return if is_ignore {
24542                NullTreatment::Ignore
24543            } else {
24544                NullTreatment::Respect
24545            };
24546        }
24547        NullTreatment::Respect
24548    }
24549
24550    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24551    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24552    /// (same shape as the `OVER` tail). Consumes the whole clause and
24553    /// returns the predicate; returns `None` when no `FILTER` follows.
24554    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24555        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24556            return Ok(None);
24557        };
24558        if !s.eq_ignore_ascii_case("filter") {
24559            return Ok(None);
24560        }
24561        self.advance(); // FILTER
24562        if !matches!(self.peek(), Token::LParen) {
24563            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24564        }
24565        self.advance(); // (
24566        if !matches!(self.peek(), Token::Where) {
24567            return Err(self.err(format!(
24568                "expected WHERE inside FILTER (...), got {:?}",
24569                self.peek()
24570            )));
24571        }
24572        self.advance(); // WHERE
24573        let cond = self.parse_expr(0)?;
24574        if !matches!(self.peek(), Token::RParen) {
24575            return Err(self.err(format!(
24576                "expected ')' to close FILTER (WHERE ...), got {:?}",
24577                self.peek()
24578            )));
24579        }
24580        self.advance(); // )
24581        Ok(Some(Box::new(cond)))
24582    }
24583
24584    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24585    /// the separator as the aggregate's second argument, which is the
24586    /// shape `string_agg` already takes. Returns whether one was there.
24587    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24588        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24589            return Ok(false);
24590        }
24591        self.advance();
24592        let Token::String(sep) = self.peek().clone() else {
24593            return Err(self.err(alloc::format!(
24594                "expected a string literal after SEPARATOR, got {:?}",
24595                self.peek()
24596            )));
24597        };
24598        self.advance();
24599        args.push(Expr::Literal(Literal::String(sep)));
24600        Ok(true)
24601    }
24602
24603    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24604    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24605    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24606    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24607    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24608        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24609            return Ok(Vec::new());
24610        };
24611        if !s.eq_ignore_ascii_case("within") {
24612            return Ok(Vec::new());
24613        }
24614        self.advance(); // WITHIN
24615        if !matches!(self.peek(), Token::Group) {
24616            return Err(self.err(format!(
24617                "expected GROUP after WITHIN, got {:?}",
24618                self.peek()
24619            )));
24620        }
24621        self.advance(); // GROUP
24622        if !matches!(self.peek(), Token::LParen) {
24623            return Err(self.err(format!(
24624                "expected '(' after WITHIN GROUP, got {:?}",
24625                self.peek()
24626            )));
24627        }
24628        self.advance(); // (
24629        if !matches!(self.peek(), Token::Order) {
24630            return Err(self.err(format!(
24631                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24632                self.peek()
24633            )));
24634        }
24635        self.advance(); // ORDER
24636        if !self.peek_is_by() {
24637            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24638        }
24639        self.advance(); // BY
24640        let mut keys: Vec<OrderBy> = Vec::new();
24641        loop {
24642            // v7.39 (round 691) — save/restore, the discipline this parser
24643            // already uses around `pending_sample_preds`, so a subquery inside
24644            // a key neither inherits nor leaks the channel.
24645            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24646            let saved_coll = self.order_key_collation.take();
24647            let parsed = self.parse_expr(0);
24648            self.in_order_by_key = saved_flag;
24649            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24650            let expr = parsed?;
24651            let desc = if matches!(self.peek(), Token::Desc) {
24652                self.advance();
24653                true
24654            } else if matches!(self.peek(), Token::Asc) {
24655                self.advance();
24656                false
24657            } else {
24658                false
24659            };
24660            let nulls_first = self.parse_optional_nulls_placement()?;
24661            keys.push(OrderBy {
24662                expr,
24663                desc,
24664                nulls_first,
24665                collation,
24666            });
24667            if matches!(self.peek(), Token::Comma) {
24668                self.advance();
24669            } else {
24670                break;
24671            }
24672        }
24673        if !matches!(self.peek(), Token::RParen) {
24674            return Err(self.err(format!(
24675                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24676                self.peek()
24677            )));
24678        }
24679        self.advance(); // )
24680        Ok(keys)
24681    }
24682
24683    /// No frame clause is supported.
24684    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24685    fn parse_over_clause(
24686        &mut self,
24687    ) -> Result<
24688        (
24689            Vec<Expr>,
24690            Vec<(Expr, bool, Option<bool>)>,
24691            Option<WindowFrame>,
24692        ),
24693        ParseError,
24694    > {
24695        // `OVER w` — a named-window reference. The WINDOW clause
24696        // parses after the select list, so the name rides out as a
24697        // marker in partition_by; parse_bare_select substitutes the
24698        // definition once the clause is known.
24699        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24700            let name = w.clone();
24701            self.advance();
24702            return Ok((
24703                alloc::vec![Expr::Column(crate::ast::ColumnName {
24704                    qualifier: Some("__named_window__".to_string()),
24705                    name,
24706                })],
24707                Vec::new(),
24708                None,
24709            ));
24710        }
24711        if !matches!(self.peek(), Token::LParen) {
24712            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24713        }
24714        self.advance();
24715        let mut partition_by = Vec::new();
24716        let mut order_by = Vec::new();
24717        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24718        // window, refined in place. PG's rules (probed against 18.4) differ
24719        // from the bare `OVER w1` form, so the reference rides out under its
24720        // own marker and `substitute_named_windows` applies them. The base
24721        // name is any leading identifier that isn't a window-spec keyword.
24722        let base_window = match self.peek() {
24723            Token::Ident(s) | Token::QuotedIdent(s)
24724                if !s.eq_ignore_ascii_case("partition")
24725                    && !s.eq_ignore_ascii_case("rows")
24726                    && !s.eq_ignore_ascii_case("range")
24727                    && !s.eq_ignore_ascii_case("groups") =>
24728            {
24729                let n = s.clone();
24730                self.advance();
24731                Some(n)
24732            }
24733            _ => None,
24734        };
24735        // PARTITION BY ?
24736        // v7.37.6-B promoted PARTITION to a reserved keyword
24737        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24738        // `Token::Ident("partition")`. Accept both so older sources
24739        // and the new lexer surface land on the same path.
24740        let is_partition_kw = match self.peek() {
24741            Token::Partition => true,
24742            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24743            _ => false,
24744        };
24745        if is_partition_kw {
24746            self.advance();
24747            if !self.peek_is_by() {
24748                return Err(self.err(format!(
24749                    "expected BY after PARTITION, got {:?}",
24750                    self.peek()
24751                )));
24752            }
24753            self.advance();
24754            loop {
24755                partition_by.push(self.parse_expr(0)?);
24756                if matches!(self.peek(), Token::Comma) {
24757                    self.advance();
24758                    continue;
24759                }
24760                break;
24761            }
24762        }
24763        // ORDER BY ?
24764        if matches!(self.peek(), Token::Order) {
24765            self.advance();
24766            if !self.peek_is_by() {
24767                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24768            }
24769            self.advance();
24770            loop {
24771                let e = self.parse_expr(0)?;
24772                let desc = if matches!(self.peek(), Token::Desc) {
24773                    self.advance();
24774                    true
24775                } else if matches!(self.peek(), Token::Asc) {
24776                    self.advance();
24777                    false
24778                } else {
24779                    false
24780                };
24781                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24782                let nulls_first = self.parse_optional_nulls_placement()?;
24783                order_by.push((e, desc, nulls_first));
24784                if matches!(self.peek(), Token::Comma) {
24785                    self.advance();
24786                    continue;
24787                }
24788                break;
24789            }
24790        }
24791        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24792        // Both keywords come through the lexer as identifiers; match
24793        // case-insensitively.
24794        let mut frame: Option<WindowFrame> = None;
24795        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24796            let kind = if s.eq_ignore_ascii_case("rows") {
24797                Some(FrameKind::Rows)
24798            } else if s.eq_ignore_ascii_case("range") {
24799                Some(FrameKind::Range)
24800            } else if s.eq_ignore_ascii_case("groups") {
24801                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24802                Some(FrameKind::Groups)
24803            } else {
24804                None
24805            };
24806            if let Some(kind) = kind {
24807                self.advance();
24808                frame = Some(self.parse_frame_tail(kind)?);
24809            }
24810        }
24811        if !matches!(self.peek(), Token::RParen) {
24812            return Err(self.err(format!(
24813                "expected ')' to close OVER clause, got {:?}",
24814                self.peek()
24815            )));
24816        }
24817        self.advance();
24818        if let Some(base) = base_window {
24819            // A copy may refine but never override the base's partitioning
24820            // (PG rejects it outright, before looking the name up).
24821            if !partition_by.is_empty() {
24822                return Err(self.err(alloc::format!(
24823                    "cannot override PARTITION BY clause of window \"{base}\""
24824                )));
24825            }
24826            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24827                qualifier: Some("__named_window_ref__".to_string()),
24828                name: base,
24829            })];
24830        }
24831        Ok((partition_by, order_by, frame))
24832    }
24833
24834    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24835    /// or `RANGE` keyword was just consumed. Accepts both
24836    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24837    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24838    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24839    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24840        let (start, end) = if matches!(self.peek(), Token::Between) {
24841            self.advance();
24842            let start = self.parse_frame_bound()?;
24843            if !matches!(self.peek(), Token::And) {
24844                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24845            }
24846            self.advance();
24847            let end = self.parse_frame_bound()?;
24848            (start, Some(end))
24849        } else {
24850            (self.parse_frame_bound()?, None)
24851        };
24852        let exclude = self.parse_frame_exclusion()?;
24853        Ok(WindowFrame {
24854            kind,
24855            start,
24856            end,
24857            exclude,
24858        })
24859    }
24860
24861    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24862    /// after a frame spec. NO OTHERS is the default no-op.
24863    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24864        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24865            return Ok(FrameExclusion::NoOthers);
24866        }
24867        self.advance(); // EXCLUDE
24868        match self.peek() {
24869            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24870                self.advance();
24871                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24872                    return Err(self.err(format!(
24873                        "expected ROW after EXCLUDE CURRENT, got {:?}",
24874                        self.peek()
24875                    )));
24876                }
24877                self.advance();
24878                Ok(FrameExclusion::CurrentRow)
24879            }
24880            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24881            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24882            // Without this arm it fell to the catch-all, whose message
24883            // self-contradictingly listed GROUP as expected.
24884            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24885                self.advance();
24886                Ok(FrameExclusion::Group)
24887            }
24888            Token::Group => {
24889                self.advance();
24890                Ok(FrameExclusion::Group)
24891            }
24892            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24893                self.advance();
24894                Ok(FrameExclusion::Ties)
24895            }
24896            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24897                self.advance();
24898                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24899                    return Err(self.err(format!(
24900                        "expected OTHERS after EXCLUDE NO, got {:?}",
24901                        self.peek()
24902                    )));
24903                }
24904                self.advance();
24905                Ok(FrameExclusion::NoOthers)
24906            }
24907            other => Err(self.err(format!(
24908                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24909            ))),
24910        }
24911    }
24912
24913    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24914    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24915    /// `UNBOUNDED FOLLOWING`.
24916    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24917        // Interval-typed offset for a value-based RANGE frame over a
24918        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24919        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24920        // PRECEDING`.
24921        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24922            let dir = self.expect_ident_like()?;
24923            return if dir.eq_ignore_ascii_case("preceding") {
24924                Ok(FrameBound::IntervalPreceding {
24925                    months,
24926                    days,
24927                    micros,
24928                })
24929            } else if dir.eq_ignore_ascii_case("following") {
24930                Ok(FrameBound::IntervalFollowing {
24931                    months,
24932                    days,
24933                    micros,
24934                })
24935            } else {
24936                Err(self.err(format!(
24937                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24938                )))
24939            };
24940        }
24941        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24942        if let Token::Integer(n) = *self.peek() {
24943            self.advance();
24944            let n: u64 = u64::try_from(n).map_err(|_| {
24945                self.err(format!(
24946                    "invalid frame offset {n} — expected non-negative integer"
24947                ))
24948            })?;
24949            let dir = self.expect_ident_like()?;
24950            return if dir.eq_ignore_ascii_case("preceding") {
24951                Ok(FrameBound::OffsetPreceding(n))
24952            } else if dir.eq_ignore_ascii_case("following") {
24953                Ok(FrameBound::OffsetFollowing(n))
24954            } else {
24955                Err(self.err(format!(
24956                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24957                )))
24958            };
24959        }
24960        let first = self.expect_ident_like()?;
24961        if first.eq_ignore_ascii_case("unbounded") {
24962            let dir = self.expect_ident_like()?;
24963            return if dir.eq_ignore_ascii_case("preceding") {
24964                Ok(FrameBound::UnboundedPreceding)
24965            } else if dir.eq_ignore_ascii_case("following") {
24966                Ok(FrameBound::UnboundedFollowing)
24967            } else {
24968                Err(self.err(format!(
24969                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24970                )))
24971            };
24972        }
24973        if first.eq_ignore_ascii_case("current") {
24974            let row = self.expect_ident_like()?;
24975            if !row.eq_ignore_ascii_case("row") {
24976                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24977            }
24978            return Ok(FrameBound::CurrentRow);
24979        }
24980        Err(self.err(format!(
24981            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24982        )))
24983    }
24984
24985    /// Detect and consume a leading interval offset in a frame bound —
24986    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24987    /// `(months, days, micros)`. Leaves the cursor on the trailing
24988    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24989    /// when the next tokens are not an interval offset.
24990    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24991        // Shape A — `INTERVAL '1 day'`.
24992        if matches!(self.peek(), Token::Interval) {
24993            self.advance(); // INTERVAL
24994            let atom = self.parse_interval_atom()?;
24995            if let Expr::Literal(Literal::Interval {
24996                months,
24997                days,
24998                micros,
24999                ..
25000            }) = atom
25001            {
25002                return Ok(Some((months, days, micros)));
25003            }
25004            return Err(self.err("expected an interval literal in frame offset".to_string()));
25005        }
25006        // Shape B — `'1 day'::interval`. Look ahead for the exact
25007        // string / `::` / interval-target triple before committing.
25008        if let Token::String(text) = self.peek() {
25009            let target_is_interval = match self.tokens.get(self.pos + 2) {
25010                Some(Token::Interval) => true,
25011                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25012                _ => false,
25013            };
25014            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25015                && target_is_interval;
25016            if is_cast {
25017                let text = text.clone();
25018                self.advance(); // string
25019                self.advance(); // ::
25020                self.advance(); // interval
25021                let parts = parse_interval_text(&text).ok_or_else(|| {
25022                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25023                })?;
25024                return Ok(Some(parts));
25025            }
25026        }
25027        Ok(None)
25028    }
25029
25030    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25031        if matches!(self.peek(), Token::Dot) {
25032            self.advance();
25033            let name = self.expect_ident_like()?;
25034            // v7.14.0 — schema-qualified function call
25035            // `<schema>.<fn>(args)`. PG dumps emit
25036            // `pg_catalog.set_config(...)` in the preamble. SPG
25037            // is single-namespace: drop the schema prefix and
25038            // route the dispatch on the bare function name.
25039            if matches!(self.peek(), Token::LParen) {
25040                return self.finish_ident_atom(name);
25041            }
25042            return Ok(Expr::Column(ColumnName {
25043                qualifier: Some(first),
25044                name,
25045            }));
25046        }
25047        if matches!(self.peek(), Token::LParen) {
25048            self.advance();
25049            // `COUNT(*)` — special-cased here because `*` isn't a normal
25050            // expression token. Lower-case match on `first` since the lexer
25051            // folds identifiers.
25052            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25053                self.advance();
25054                if !matches!(self.peek(), Token::RParen) {
25055                    return Err(self.err(format!(
25056                        "expected ')' after COUNT(*), got {:?}",
25057                        self.peek()
25058                    )));
25059                }
25060                self.advance();
25061                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25062                let filter = self.parse_filter_clause()?;
25063                // v4.12: COUNT(*) OVER (...) — same window tail.
25064                let null_treatment = self.parse_null_treatment_modifier();
25065                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25066                    && s.eq_ignore_ascii_case("over")
25067                {
25068                    self.advance();
25069                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25070                    return Ok(Expr::WindowFunction {
25071                        name: "count_star".into(),
25072                        args: Vec::new(),
25073                        partition_by,
25074                        order_by,
25075                        frame,
25076                        null_treatment,
25077                        filter,
25078                    });
25079                }
25080                if let Some(filter) = filter {
25081                    return Ok(Expr::AggregateOrdered {
25082                        call: Box::new(Expr::FunctionCall {
25083                            name: "count_star".into(),
25084                            args: Vec::new(),
25085                        }),
25086                        order_by: Vec::new(),
25087                        distinct: false,
25088                        filter: Some(filter),
25089                    });
25090                }
25091                return Ok(Expr::FunctionCall {
25092                    name: "count_star".into(),
25093                    args: Vec::new(),
25094                });
25095            }
25096            // Function call. PG-style: zero-or-more comma-separated args.
25097            let mut args = Vec::new();
25098            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25099            // Names are collected in lock-step with `args` and resolved to
25100            // positional order after the loop (the AST stays positional).
25101            let mut arg_names: Vec<Option<String>> = Vec::new();
25102            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25103            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25104            // seen, so the value arguments before it can be folded.
25105            let mut saw_separator = false;
25106            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25107            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25108            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25109            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25110                self.advance();
25111                true
25112            } else if matches!(self.peek(), Token::All) {
25113                self.advance();
25114                false
25115            } else {
25116                false
25117            };
25118            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25119            // TIMESTAMPDIFF take a bare unit keyword as the first
25120            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25121            // bare type keyword (DATE / TIME / DATETIME); lower them
25122            // onto string literals so the evaluator sees plain text.
25123            if ((first.eq_ignore_ascii_case("timestampadd")
25124                || first.eq_ignore_ascii_case("timestampdiff"))
25125                && matches!(self.peek(), Token::Ident(u) if matches!(
25126                    u.to_ascii_lowercase().as_str(),
25127                    "microsecond" | "second" | "minute" | "hour" | "day"
25128                        | "week" | "month" | "quarter" | "year"
25129                )))
25130                || (first.eq_ignore_ascii_case("get_format")
25131                    && matches!(self.peek(), Token::Ident(u) if matches!(
25132                        u.to_ascii_lowercase().as_str(),
25133                        "date" | "time" | "datetime" | "timestamp"
25134                    )))
25135            {
25136                if let Token::Ident(u) = self.peek() {
25137                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25138                }
25139                self.advance();
25140                if matches!(self.peek(), Token::Comma) {
25141                    self.advance();
25142                }
25143            }
25144            // `ROW(a, b, …)` keyword constructor. Followed by a
25145            // comparison operator or [NOT] IN it joins the paren
25146            // row-constructor machinery (fieldwise parse-time
25147            // expansion); bare, it stays a `row` call the evaluator
25148            // renders as PG record text.
25149            if first.eq_ignore_ascii_case("row") {
25150                let mut row_items = Vec::new();
25151                if !matches!(self.peek(), Token::RParen) {
25152                    loop {
25153                        row_items.push(self.parse_expr(0)?);
25154                        match self.peek() {
25155                            Token::Comma => {
25156                                self.advance();
25157                            }
25158                            Token::RParen => break,
25159                            other => {
25160                                return Err(self.err(format!(
25161                                    "expected ',' or ')' in ROW(...), got {other:?}"
25162                                )));
25163                            }
25164                        }
25165                    }
25166                }
25167                self.advance(); // ')'
25168                let comparison_follows = matches!(
25169                    self.peek(),
25170                    Token::Eq
25171                        | Token::NotEq
25172                        | Token::Lt
25173                        | Token::LtEq
25174                        | Token::Gt
25175                        | Token::GtEq
25176                        | Token::In
25177                ) || (matches!(self.peek(), Token::Not)
25178                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25179                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25180                if comparison_follows && !row_items.is_empty() {
25181                    return self.parse_row_comparison_tail(row_items);
25182                }
25183                return Ok(Expr::FunctionCall {
25184                    name: String::from("row"),
25185                    args: row_items,
25186                });
25187            }
25188            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25189            // the parse-mode keyword introduces the source text. SPG
25190            // carries XML as text, so both modes lower to __xmlparse(expr)
25191            // which validates well-formedness and returns Value::Xml.
25192            if first.eq_ignore_ascii_case("xmlparse")
25193                && matches!(self.peek(), Token::Ident(kw)
25194                    if kw.eq_ignore_ascii_case("document")
25195                        || kw.eq_ignore_ascii_case("content"))
25196            {
25197                let mode = match self.advance() {
25198                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25199                    _ => unreachable!("peeked an ident"),
25200                };
25201                let src = self.parse_expr(0)?;
25202                if !matches!(self.peek(), Token::RParen) {
25203                    return Err(self.err(format!(
25204                        "expected ')' to close XMLPARSE, got {:?}",
25205                        self.peek()
25206                    )));
25207                }
25208                self.advance();
25209                return Ok(Expr::FunctionCall {
25210                    name: String::from("__xmlparse"),
25211                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25212                });
25213            }
25214            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25215            // keyword introduces the element name (a bare or quoted
25216            // identifier), then optional content expressions. Lower to a
25217            // plain `xmlelement(name_text, content …)` call.
25218            if first.eq_ignore_ascii_case("xmlelement")
25219                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25220            {
25221                self.advance(); // consume NAME
25222                let elem_name = match self.peek().clone() {
25223                    Token::Ident(n) | Token::QuotedIdent(n) => {
25224                        self.advance();
25225                        n
25226                    }
25227                    other => {
25228                        return Err(self.err(format!(
25229                            "expected element name after XMLELEMENT NAME, got {other:?}"
25230                        )));
25231                    }
25232                };
25233                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25234                while matches!(self.peek(), Token::Comma) {
25235                    self.advance();
25236                    args.push(self.parse_expr(0)?);
25237                }
25238                if !matches!(self.peek(), Token::RParen) {
25239                    return Err(self.err(format!(
25240                        "expected ')' to close XMLELEMENT, got {:?}",
25241                        self.peek()
25242                    )));
25243                }
25244                self.advance();
25245                return Ok(Expr::FunctionCall {
25246                    name: String::from("xmlelement"),
25247                    args,
25248                });
25249            }
25250            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25251            // becomes a `<name>value</name>` element; a bare column infers its
25252            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25253            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25254                let mut args: Vec<Expr> = Vec::new();
25255                loop {
25256                    let val = self.parse_expr(0)?;
25257                    let name = if matches!(self.peek(), Token::As) {
25258                        self.advance();
25259                        match self.peek().clone() {
25260                            Token::Ident(n) | Token::QuotedIdent(n) => {
25261                                self.advance();
25262                                n
25263                            }
25264                            other => {
25265                                return Err(self.err(format!(
25266                                    "expected name after AS in XMLFOREST, got {other:?}"
25267                                )));
25268                            }
25269                        }
25270                    } else if let Expr::Column(c) = &val {
25271                        c.name.clone()
25272                    } else {
25273                        return Err(
25274                            self.err("XMLFOREST element without a column name needs AS".into())
25275                        );
25276                    };
25277                    args.push(Expr::Literal(Literal::String(name)));
25278                    args.push(val);
25279                    if matches!(self.peek(), Token::Comma) {
25280                        self.advance();
25281                    } else {
25282                        break;
25283                    }
25284                }
25285                if !matches!(self.peek(), Token::RParen) {
25286                    return Err(self.err(format!(
25287                        "expected ')' to close XMLFOREST, got {:?}",
25288                        self.peek()
25289                    )));
25290                }
25291                self.advance();
25292                return Ok(Expr::FunctionCall {
25293                    name: String::from("xmlforest"),
25294                    args,
25295                });
25296            }
25297            // SQL-standard `POSITION(sub IN str)` — lowers onto
25298            // strpos(str, sub). IN is the argument separator here,
25299            // so the needle parses with the IN-tail suppressed.
25300            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25301                let saved = self.suppress_in_tail;
25302                self.suppress_in_tail = true;
25303                let needle = self.parse_expr(0);
25304                self.suppress_in_tail = saved;
25305                let needle = needle?;
25306                if matches!(self.peek(), Token::In) {
25307                    self.advance();
25308                    let haystack = self.parse_expr(0)?;
25309                    if !matches!(self.peek(), Token::RParen) {
25310                        return Err(self.err(format!(
25311                            "expected ')' to close POSITION, got {:?}",
25312                            self.peek()
25313                        )));
25314                    }
25315                    self.advance();
25316                    return Ok(Expr::FunctionCall {
25317                        name: String::from("strpos"),
25318                        args: alloc::vec![haystack, needle],
25319                    });
25320                }
25321                // position(sub, str) comma form (incl. bytea) —
25322                // hand the parsed first arg to the generic list.
25323                args.push(needle);
25324                if matches!(self.peek(), Token::Comma) {
25325                    self.advance();
25326                }
25327            }
25328            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25329            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25330            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25331            // riding the generic argument list below.
25332            if first.eq_ignore_ascii_case("trim") {
25333                let mode = match self.peek() {
25334                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25335                        self.advance();
25336                        Some("btrim")
25337                    }
25338                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25339                        self.advance();
25340                        Some("ltrim")
25341                    }
25342                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25343                        self.advance();
25344                        Some("rtrim")
25345                    }
25346                    _ => None,
25347                };
25348                if mode.is_some() || matches!(self.peek(), Token::From) {
25349                    // TRIM([mode] FROM str) — no strip-chars.
25350                    let chars = if matches!(self.peek(), Token::From) {
25351                        None
25352                    } else {
25353                        Some(self.parse_expr(0)?)
25354                    };
25355                    if !matches!(self.peek(), Token::From) {
25356                        return Err(self.err(format!(
25357                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25358                            self.peek()
25359                        )));
25360                    }
25361                    self.advance();
25362                    let target = self.parse_expr(0)?;
25363                    if !matches!(self.peek(), Token::RParen) {
25364                        return Err(
25365                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25366                        );
25367                    }
25368                    self.advance();
25369                    let mut trim_args = alloc::vec![target];
25370                    if let Some(c) = chars {
25371                        trim_args.push(c);
25372                    }
25373                    return Ok(Expr::FunctionCall {
25374                        name: String::from(mode.unwrap_or("btrim")),
25375                        args: trim_args,
25376                    });
25377                }
25378            }
25379            if !matches!(self.peek(), Token::RParen) {
25380                loop {
25381                    // v7.38 (read01, T14) — `argname => value` names this arg.
25382                    // v7.39 (read01 round 77) — `argname := value` is the same
25383                    // thing, and it is the spelling PG's own docs lead with. It
25384                    // was simply never lexed here, so every `f(x := 1)` died in
25385                    // the parser regardless of what `f` was.
25386                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25387                        (
25388                            Token::Ident(n) | Token::QuotedIdent(n),
25389                            Some(Token::FatArrow | Token::ColonEq),
25390                        ) => {
25391                            let name = n.clone();
25392                            self.advance(); // name
25393                            self.advance(); // => / :=
25394                            Some(name)
25395                        }
25396                        _ => None,
25397                    };
25398                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25399                    // array's elements into a variadic call's trailing args
25400                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25401                    // reserved, so it arrives as a bare ident before the arg.
25402                    let is_variadic = this_name.is_none()
25403                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25404                    if is_variadic {
25405                        self.advance();
25406                    }
25407                    let arg = self.parse_expr(0)?;
25408                    args.push(match &this_name {
25409                        // The callee's parameter names decide the slot, and a
25410                        // user function's live in the catalog. Carry the name
25411                        // to eval rather than guessing here.
25412                        Some(n) => Expr::NamedArg {
25413                            name: n.clone(),
25414                            expr: Box::new(arg),
25415                        },
25416                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25417                        None => arg,
25418                    });
25419                    arg_names.push(this_name);
25420                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25421                    // The `::` cast already worked; this lowers the
25422                    // function form onto the same Expr::Cast node.
25423                    if first.eq_ignore_ascii_case("cast")
25424                        && args.len() == 1
25425                        && matches!(self.peek(), Token::As)
25426                    {
25427                        self.advance();
25428                        let target = self.parse_cast_target()?;
25429                        if !matches!(self.peek(), Token::RParen) {
25430                            return Err(self.err(format!(
25431                                "expected ')' to close CAST, got {:?}",
25432                                self.peek()
25433                            )));
25434                        }
25435                        self.advance();
25436                        return Ok(Expr::Cast {
25437                            expr: Box::new(args.pop().expect("one arg")),
25438                            target,
25439                        });
25440                    }
25441                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25442                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25443                    // keywords; SPG's lexer makes them plain idents (so they'd be
25444                    // read as column refs). Lower the keyword to the string form
25445                    // the evaluator already accepts.
25446                    if first.eq_ignore_ascii_case("normalize")
25447                        && args.len() == 1
25448                        && matches!(self.peek(), Token::Comma)
25449                    {
25450                        let form = match self.tokens.get(self.pos + 1) {
25451                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25452                                let up = f.to_ascii_uppercase();
25453                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25454                            }
25455                            _ => None,
25456                        };
25457                        if let Some(up) = form {
25458                            self.advance(); // comma
25459                            self.advance(); // form keyword
25460                            args.push(Expr::Literal(Literal::String(up)));
25461                        }
25462                    }
25463                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25464                    // form. Desugars to the comma-list shape evaluator already
25465                    // handles. Triggered after the first arg when the function
25466                    // name is substring / substr and the next token is FROM
25467                    // (a reserved keyword in PG; SPG also reserves it).
25468                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25469                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25470                    // internal __substring_similar(str, pat, esc) call.
25471                    if (first.eq_ignore_ascii_case("substring")
25472                        || first.eq_ignore_ascii_case("substr"))
25473                        && args.len() == 1
25474                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25475                    {
25476                        self.advance(); // SIMILAR
25477                        let pattern = self.parse_expr(0)?;
25478                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25479                        {
25480                            return Err(self.err(format!(
25481                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25482                                self.peek()
25483                            )));
25484                        }
25485                        self.advance(); // ESCAPE
25486                        let esc = self.parse_expr(0)?;
25487                        if !matches!(self.peek(), Token::RParen) {
25488                            return Err(self.err(format!(
25489                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25490                                self.peek()
25491                            )));
25492                        }
25493                        self.advance();
25494                        args.push(pattern);
25495                        args.push(esc);
25496                        return Ok(Expr::FunctionCall {
25497                            name: "__substring_similar".to_string(),
25498                            args,
25499                        });
25500                    }
25501                    if (first.eq_ignore_ascii_case("substring")
25502                        || first.eq_ignore_ascii_case("substr"))
25503                        && args.len() == 1
25504                        && matches!(self.peek(), Token::From | Token::For)
25505                    {
25506                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25507                        // `substring(str FOR len)` which PG treats as FROM 1.
25508                        if matches!(self.peek(), Token::From) {
25509                            self.advance();
25510                            let start = self.parse_expr(0)?;
25511                            args.push(start);
25512                        } else {
25513                            args.push(Expr::Literal(Literal::Integer(1)));
25514                        }
25515                        if matches!(self.peek(), Token::For) {
25516                            self.advance();
25517                            let length = self.parse_expr(0)?;
25518                            args.push(length);
25519                        }
25520                        if !matches!(self.peek(), Token::RParen) {
25521                            return Err(self.err(format!(
25522                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25523                                self.peek()
25524                            )));
25525                        }
25526                        self.advance();
25527                        return Ok(Expr::FunctionCall {
25528                            name: first.to_ascii_lowercase(),
25529                            args,
25530                        });
25531                    }
25532                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25533                    // syntactic form. Desugars to the `overlay(str,
25534                    // repl, n[, len])` comma-list shape the evaluator
25535                    // already implements. `PLACING` is not a reserved
25536                    // token in SPG, so it arrives as a bare Ident.
25537                    if first.eq_ignore_ascii_case("overlay")
25538                        && args.len() == 1
25539                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25540                    {
25541                        self.advance(); // consume PLACING
25542                        args.push(self.parse_expr(0)?); // replacement
25543                        if !matches!(self.peek(), Token::From) {
25544                            return Err(self.err(format!(
25545                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25546                                self.peek()
25547                            )));
25548                        }
25549                        self.advance();
25550                        args.push(self.parse_expr(0)?); // start position
25551                        if matches!(self.peek(), Token::For) {
25552                            self.advance();
25553                            args.push(self.parse_expr(0)?); // length
25554                        }
25555                        if !matches!(self.peek(), Token::RParen) {
25556                            return Err(self.err(format!(
25557                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25558                                self.peek()
25559                            )));
25560                        }
25561                        self.advance();
25562                        return Ok(Expr::FunctionCall {
25563                            name: String::from("overlay"),
25564                            args,
25565                        });
25566                    }
25567                    // `TRIM(chars FROM str)` — the keyword-less
25568                    // spelling lands here after the chars parse
25569                    // (the keyword forms return earlier).
25570                    if first.eq_ignore_ascii_case("trim")
25571                        && args.len() == 1
25572                        && matches!(self.peek(), Token::From)
25573                    {
25574                        self.advance();
25575                        let target = self.parse_expr(0)?;
25576                        if !matches!(self.peek(), Token::RParen) {
25577                            return Err(self.err(format!(
25578                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25579                                self.peek()
25580                            )));
25581                        }
25582                        self.advance();
25583                        let chars = args.pop().expect("one arg");
25584                        return Ok(Expr::FunctionCall {
25585                            name: String::from("btrim"),
25586                            args: alloc::vec![target, chars],
25587                        });
25588                    }
25589                    // v7.24 (round-16 A) — aggregate-internal
25590                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25591                    // LAST)`. Keys close the argument list.
25592                    if matches!(self.peek(), Token::Order) {
25593                        self.advance();
25594                        if !self.peek_is_by() {
25595                            return Err(self.err(format!(
25596                                "expected BY after ORDER in aggregate args, got {:?}",
25597                                self.peek()
25598                            )));
25599                        }
25600                        self.advance();
25601                        loop {
25602                            // v7.39 (round 691) — save/restore, the discipline this parser
25603                            // already uses around `pending_sample_preds`, so a subquery inside
25604                            // a key neither inherits nor leaks the channel.
25605                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25606                            let saved_coll = self.order_key_collation.take();
25607                            let parsed = self.parse_expr(0);
25608                            self.in_order_by_key = saved_flag;
25609                            let collation =
25610                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25611                            let expr = parsed?;
25612                            let desc = if matches!(self.peek(), Token::Desc) {
25613                                self.advance();
25614                                true
25615                            } else if matches!(self.peek(), Token::Asc) {
25616                                self.advance();
25617                                false
25618                            } else {
25619                                false
25620                            };
25621                            let nulls_first = self.parse_optional_nulls_placement()?;
25622                            agg_order_by.push(OrderBy {
25623                                expr,
25624                                desc,
25625                                nulls_first,
25626                                collation,
25627                            });
25628                            if matches!(self.peek(), Token::Comma) {
25629                                self.advance();
25630                            } else {
25631                                break;
25632                            }
25633                        }
25634                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25635                        // follow the ORDER BY inside GROUP_CONCAT.
25636                        if self.consume_group_concat_separator(&mut args)? {
25637                            saw_separator = true;
25638                        }
25639                        if !matches!(self.peek(), Token::RParen) {
25640                            return Err(self.err(format!(
25641                                "expected ')' after aggregate ORDER BY, got {:?}",
25642                                self.peek()
25643                            )));
25644                        }
25645                        break;
25646                    }
25647                    // v7.39 (round 354, M12) — …or directly after the
25648                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25649                    // own spelling of what PG passes as string_agg's second
25650                    // argument; it was a parse error, so every MySQL query
25651                    // that names its own separator failed outright.
25652                    if self.consume_group_concat_separator(&mut args)? {
25653                        saw_separator = true;
25654                        break;
25655                    }
25656                    match self.peek() {
25657                        Token::Comma => {
25658                            self.advance();
25659                        }
25660                        Token::RParen => break,
25661                        other => {
25662                            return Err(self.err(format!(
25663                                "expected ',' or ')' in function args, got {other:?}"
25664                            )));
25665                        }
25666                    }
25667                }
25668            }
25669            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25670            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25671            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25672            // meaning a separator — that is what the explicit SEPARATOR
25673            // tail is for. Fold them into one `concat(...)` so the
25674            // aggregate keeps its single value argument.
25675            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25676                let values = args.len() - usize::from(saw_separator);
25677                if values > 1 {
25678                    let sep_arg = if saw_separator { args.pop() } else { None };
25679                    let folded = Expr::FunctionCall {
25680                        name: "concat".to_string(),
25681                        args: core::mem::take(&mut args),
25682                    };
25683                    args.push(folded);
25684                    if let Some(sep) = sep_arg {
25685                        args.push(sep);
25686                    }
25687                }
25688            }
25689            self.advance(); // consume ')'
25690            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25691            // any more. The parser has no catalog, so it could only ever resolve
25692            // the handful of `make_*` builtins whose parameter names were baked
25693            // into a table right here — every user function got
25694            // "does not support named arguments", though the catalog has been
25695            // storing its parameter names all along. Reordering happens in eval,
25696            // in one place, for builtins and user functions alike.
25697            // v7.32 (round-29) — ordered-set aggregate tail
25698            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25699            // (percentile_cont / percentile_disc / mode). The sort spec
25700            // lands in the same `order_by` slot a decorated aggregate
25701            // uses; the executor dispatches on the function name. WITHIN
25702            // GROUP and an intra-argument ORDER BY are mutually
25703            // exclusive (PG rejects both).
25704            let within_group_order = self.parse_within_group_clause()?;
25705            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25706                return Err(self.err(
25707                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25708                        .into(),
25709                ));
25710            }
25711            let within_group_seen = !within_group_order.is_empty();
25712            let agg_order_by = if within_group_order.is_empty() {
25713                agg_order_by
25714            } else {
25715                within_group_order
25716            };
25717            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25718            let filter = self.parse_filter_clause()?;
25719            // v4.12: window-function tail — `name(args) OVER (...)`.
25720            // Promotes the just-parsed FunctionCall into a
25721            // WindowFunction node carrying partition + order.
25722            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25723            // / `RESPECT NULLS OVER (...)` between the closing paren
25724            // and `OVER`.
25725            let null_treatment = self.parse_null_treatment_modifier();
25726            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25727                && s.eq_ignore_ascii_case("over")
25728            {
25729                self.advance();
25730                // v7.39 (round 230) — PG implements neither modifier for a
25731                // windowed call and says so (0A000). Both used to be parsed
25732                // and then silently dropped here, so `count(DISTINCT v)
25733                // OVER (…)` quietly answered the non-distinct count.
25734                if agg_distinct {
25735                    return Err(
25736                        self.err("DISTINCT is not implemented for window functions".to_string())
25737                    );
25738                }
25739                if !agg_order_by.is_empty() {
25740                    // PG separates the two shapes that land here: a
25741                    // WITHIN GROUP call is an ordered-set aggregate and gets
25742                    // its own message naming the aggregate; a plain
25743                    // `agg(x ORDER BY y)` gets the generic one.
25744                    let msg = if within_group_seen {
25745                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25746                    } else {
25747                        "aggregate ORDER BY is not implemented for window functions".to_string()
25748                    };
25749                    return Err(self.err(msg));
25750                }
25751                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25752                return Ok(Expr::WindowFunction {
25753                    name: first,
25754                    args,
25755                    partition_by,
25756                    order_by,
25757                    frame,
25758                    null_treatment,
25759                    filter,
25760                });
25761            }
25762            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25763                return Ok(Expr::AggregateOrdered {
25764                    call: Box::new(Expr::FunctionCall { name: first, args }),
25765                    order_by: agg_order_by,
25766                    distinct: agg_distinct,
25767                    filter,
25768                });
25769            }
25770            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25771            // over TIMESTAMPTZ and has no timestamp overload, so a
25772            // timestamp argument is coerced on the way in and the answer
25773            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25774            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25775            // zone`. SPG answered `timestamp without time zone`, dropping
25776            // the offset from every rendering.
25777            //
25778            // Writing the coercion PG performs makes the existing
25779            // argument-driven typing (the one `date_trunc` uses) reach the
25780            // right answer, rather than teaching the type layer a second
25781            // rule. MySQL's DATE_ADD is a different function that returns
25782            // DATE or DATETIME, so this is PG-dialect only.
25783            //
25784            // Out-of-line because this sits on the RECURSIVE descent
25785            // frame: an inline block with locals here costs every nesting
25786            // level, and the suite's deep-nesting sentinel overflowed the
25787            // 512 KiB parser stack the moment one was added (round 430's
25788            // lesson, in the same shape).
25789            if !self.mysql_dialect {
25790                lift_date_add_arg_to_timestamptz(&first, &mut args);
25791            }
25792            return Ok(Expr::FunctionCall { name: first, args });
25793        }
25794        // v7.9.20 — SQL-standard parenless keyword expressions
25795        // (PG treats these as functions called without parens).
25796        // Resolve to a synthetic FunctionCall so the engine's
25797        // eval path reuses the existing function-call routing.
25798        // mailrs G3.
25799        let lc = first.to_ascii_lowercase();
25800        if matches!(
25801            lc.as_str(),
25802            "current_date"
25803                | "current_time"
25804                | "current_timestamp"
25805                | "localtimestamp"
25806                | "localtime"
25807                // v7.37.17 (17.6 siblings) — session-identity SQL-
25808                // standard parenless keywords. current_user /
25809                // session_user / user were already caught by the
25810                // pgwire canned-response shortcut but bare-select
25811                // in the embedded engine went through Expr::Column
25812                // and errored. Adding them here so the parser
25813                // resolves to a synthetic FunctionCall that reuses
25814                // the existing eval/functions.rs dispatch.
25815                | "current_user"
25816                | "session_user"
25817                | "current_role"
25818                | "current_catalog"
25819                | "current_schema"
25820                | "current_database"
25821                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25822                | "system_user"
25823        ) {
25824            return Ok(Expr::FunctionCall {
25825                name: lc,
25826                args: Vec::new(),
25827            });
25828        }
25829        Ok(Expr::Column(ColumnName {
25830            qualifier: None,
25831            name: first,
25832        }))
25833    }
25834}
25835
25836/// v7.39 (round 522) — write the coercion PG's `date_add` /
25837/// `date_subtract` signature performs.
25838///
25839/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25840/// timestamp argument is cast on the way in and the answer is
25841/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25842/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25843/// `timestamp without time zone`, dropping the offset from every
25844/// rendering of the result.
25845///
25846/// Writing the cast the signature implies lets the existing
25847/// argument-driven typing (the one `date_trunc` uses) reach the right
25848/// answer instead of teaching the type layer a second rule. MySQL's
25849/// DATE_ADD is a different function returning DATE or DATETIME, so the
25850/// caller applies this in PG dialect only.
25851///
25852/// A free function, and not a block at the call site, because the caller
25853/// is on the recursive-descent frame chain.
25854#[inline(never)]
25855fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25856    if args.len() != 2
25857        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25858    {
25859        return;
25860    }
25861    let base = args.remove(0);
25862    args.insert(
25863        0,
25864        Expr::Cast {
25865            expr: Box::new(base),
25866            target: CastTarget::Timestamptz,
25867        },
25868    );
25869}
25870
25871/// v6.8.2 — walk an expression tree and return the first column
25872/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25873/// to derive `CreateIndexStatement.column` from an expression
25874/// key (so downstream planner code resolving a primary column
25875/// position keeps working with expression indexes). Returns
25876/// `None` when the expression has no column ref at all — caller
25877/// surfaces that as a parse error.
25878fn extract_first_column(expr: &Expr) -> Option<String> {
25879    match expr {
25880        Expr::Column(cn) => Some(cn.name.clone()),
25881        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25882        Expr::Binary { lhs, rhs, .. } => {
25883            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25884        }
25885        Expr::Unary { expr: e, .. } => extract_first_column(e),
25886        // v7.39 (read01 round 93) — a cast wraps its operand: a common
25887        // expression-index key is `lower(col::text)`, where the column
25888        // sits under the `::text` cast inside the function arg. Without
25889        // descending here the key was rejected as "references no column".
25890        Expr::Cast { expr: e, .. } => extract_first_column(e),
25891        _ => None,
25892    }
25893}
25894
25895fn maybe_not(expr: Expr, negated: bool) -> Expr {
25896    if negated {
25897        Expr::Unary {
25898            op: UnOp::Not,
25899            expr: Box::new(expr),
25900        }
25901    } else {
25902        expr
25903    }
25904}
25905
25906/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25907/// things in the two dialects, and SPG read all three PG's way:
25908///
25909/// | token | PG (and SPG) | MySQL, measured |
25910/// |---|---|---|
25911/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25912/// | `&&` | inet / array overlap | **AND** |
25913/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25914///
25915/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25916/// answer with no error, which is why they are routed here rather than
25917/// left to the shared table.
25918impl Parser {
25919    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25920        if self.mysql_dialect {
25921            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25922            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25923            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25924            if let Token::Ident(w) = tok
25925                && w.eq_ignore_ascii_case("div")
25926            {
25927                return Some((BinOp::IntDiv, 8));
25928            }
25929            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25930            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25931            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25932            // there sits in operand position, not infix).
25933            if let Token::Ident(w) = tok
25934                && w.eq_ignore_ascii_case("mod")
25935            {
25936                return Some((BinOp::Mod, 8));
25937            }
25938            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25939            // plain ident to the lexer. Its precedence sits between OR (1)
25940            // and AND (3) — hence rung 2, the slot freed by moving AND up.
25941            if let Token::Ident(w) = tok
25942                && w.eq_ignore_ascii_case("xor")
25943            {
25944                return Some((BinOp::LogicalXor, 2));
25945            }
25946            match tok {
25947                Token::Concat => return Some((BinOp::Or, 1)),
25948                // MySQL's `&&` is logical AND, sharing AND's rung (3).
25949                Token::InetOverlap => return Some((BinOp::And, 3)),
25950                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25951                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25952                _ => {}
25953            }
25954        }
25955        binop_from(tok)
25956    }
25957}
25958
25959// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25960// (which sits strictly between OR and AND), every level from AND upward was
25961// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25962// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25963// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25964// the *relative* order of every PG operator is unchanged by the shift.
25965fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25966    let pair = match tok {
25967        Token::Or => (BinOp::Or, 1),
25968        Token::And => (BinOp::And, 3),
25969        Token::Eq => (BinOp::Eq, 5),
25970        Token::NotEq => (BinOp::NotEq, 5),
25971        Token::Lt => (BinOp::Lt, 5),
25972        Token::LtEq => (BinOp::LtEq, 5),
25973        Token::Gt => (BinOp::Gt, 5),
25974        Token::GtEq => (BinOp::GtEq, 5),
25975        // pgvector distance ops all sit on the same rung — tighter than
25976        // comparisons (5) so `col <-> v < threshold` parses correctly.
25977        Token::L2Distance => (BinOp::L2Distance, 6),
25978        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25979        // comparison rung.
25980        Token::GeomParallel => (BinOp::GeomParallel, 5),
25981        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25982        // comparison rung.
25983        Token::OverLeft => (BinOp::OverLeft, 5),
25984        Token::OverRight => (BinOp::OverRight, 5),
25985        Token::GeomPerp => (BinOp::GeomPerp, 5),
25986        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25987        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25988        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25989        Token::InnerProduct => (BinOp::InnerProduct, 6),
25990        Token::CosineDistance => (BinOp::CosineDistance, 6),
25991        Token::Plus => (BinOp::Add, 7),
25992        Token::Minus => (BinOp::Sub, 7),
25993        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25994        // binds every "other" operator (`||`, `|`, `&`, `#`, the
25995        // pgvector distances above) BETWEEN additive (7) and the
25996        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25997        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25998        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25999        // ("matches PG conceptually" — the round-753 audit measured it
26000        // false; the old rung errored on `'a' || 1 + 1` with
26001        // `text + integer`). Same-level chains left-fold, as PG does.
26002        Token::Concat => (BinOp::Concat, 6),
26003        Token::Pipe => (BinOp::BitOr, 6),
26004        Token::Amp => (BinOp::BitAnd, 6),
26005        Token::Star => (BinOp::Mul, 8),
26006        Token::Slash => (BinOp::Div, 8),
26007        Token::Percent => (BinOp::Mod, 8),
26008        // v4.14: JSON path ops bind tighter than comparisons (5)
26009        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26010        // Same rung as the multiplicative ops.
26011        Token::JsonGet => (BinOp::JsonGet, 8),
26012        Token::JsonGetText => (BinOp::JsonGetText, 8),
26013        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26014        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26015        Token::JsonContains => (BinOp::JsonContains, 8),
26016        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26017        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26018        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26019        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26020        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26021        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26022        // v7.12.2 — `@@` binds at the comparison rung (looser than
26023        // arithmetic, tighter than AND / OR). PG places `@@` at
26024        // the same precedence as `=` / `<`, so we follow.
26025        Token::TsMatch => (BinOp::TsMatch, 5),
26026        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26027        // PG places these at the comparison rung (same level as `=`),
26028        // so we follow.
26029        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26030        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26031        Token::InetContains => (BinOp::InetContains, 5),
26032        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26033        Token::InetOverlap => (BinOp::InetOverlap, 5),
26034        // v7.39 (round 508) — the geometric and pattern-order predicates
26035        // ride the comparison rung, as every other predicate does.
26036        Token::Intersects => (BinOp::Intersects, 5),
26037        Token::IsBelow => (BinOp::IsBelow, 5),
26038        Token::IsAbove => (BinOp::IsAbove, 5),
26039        Token::PatternLt => (BinOp::PatternLt, 5),
26040        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26041        Token::PatternGt => (BinOp::PatternGt, 5),
26042        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26043        // `@@@` is the old spelling of `@@` and means exactly it.
26044        Token::TsMatchOld => (BinOp::TsMatch, 5),
26045        _ => return None,
26046    };
26047    Some(pair)
26048}
26049
26050#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26051// `as f32` here is intentional: vector elements widen / narrow into f32 on
26052// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26053// past ~15 decimal digits — both are acceptable for a fixed-precision
26054// pgvector column.
26055/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26056/// implicit table alias and break trailing clauses. WITH lands
26057/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26058/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26059/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26060/// / VALUES / FOR / LATERAL — all of which would otherwise be
26061/// silently swallowed by `parse_optional_alias`.
26062fn is_alias_stopword(s: &str) -> bool {
26063    matches!(
26064        s.to_ascii_lowercase().as_str(),
26065        "with"
26066            | "on"
26067            | "where"
26068            | "having"
26069            | "group"
26070            | "order"
26071            | "limit"
26072            | "offset"
26073            | "union"
26074            | "except"
26075            | "intersect"
26076            | "returning"
26077            | "set"
26078            | "values"
26079            | "for"
26080            | "window"
26081            | "tablesample"
26082            | "lateral"
26083            | "left"
26084            | "right"
26085            | "inner"
26086            | "outer"
26087            | "full"
26088            | "cross"
26089            | "join"
26090            | "natural"
26091            | "using"
26092            | "fetch"
26093    )
26094}
26095
26096fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26097    match e {
26098        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26099        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26100        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26101        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26102        // so scale the divisor by hand instead of `f32::powi`.)
26103        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26104            let mut div = 1.0f32;
26105            for _ in 0..*scale {
26106                div *= 10.0;
26107            }
26108            Some(*unscaled as f32 / div)
26109        }
26110        Expr::Unary {
26111            op: UnOp::Neg,
26112            expr,
26113        } => extract_numeric_literal(expr).map(|x| -x),
26114        _ => None,
26115    }
26116}
26117
26118/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26119/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26120/// negative. Returns `None` if any pair fails to parse or no pair is found.
26121///
26122/// Recognised units (case-insensitive, optional trailing `s`):
26123/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26124/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26125/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26126/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26127/// (PG-canonical: DST and month-boundary semantics depend on this).
26128/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26129/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26130/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26131#[allow(clippy::cast_possible_truncation)]
26132fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26133    let mut months: i64 = 0;
26134    let mut days: i64 = 0;
26135    let mut micros: i64 = 0;
26136    let mut in_time = false;
26137    let mut num = alloc::string::String::new();
26138    for ch in rest.chars() {
26139        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26140            num.push(ch);
26141            continue;
26142        }
26143        if ch == 'T' || ch == 't' {
26144            if !num.is_empty() {
26145                return None;
26146            }
26147            in_time = true;
26148            continue;
26149        }
26150        let n: f64 = num.parse().ok()?;
26151        num.clear();
26152        match (ch, in_time) {
26153            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26154            ('M', false) => months += n as i64,
26155            ('W' | 'w', false) => days += (n * 7.0) as i64,
26156            ('D' | 'd', false) => days += n as i64,
26157            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26158            ('M', true) => micros += (n * 60_000_000.0) as i64,
26159            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26160            _ => return None,
26161        }
26162    }
26163    if !num.is_empty() {
26164        return None;
26165    }
26166    Some((
26167        i32::try_from(months).ok()?,
26168        i32::try_from(days).ok()?,
26169        micros,
26170    ))
26171}
26172
26173/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26174/// leading `-` negates the whole value). Rejects date-like strings.
26175fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26176    let (neg, body) = match s.strip_prefix('-') {
26177        Some(b) => (true, b),
26178        None => (false, s),
26179    };
26180    let (y, m) = body.split_once('-')?;
26181    let years: i32 = y.parse().ok()?;
26182    let mons: i32 = m.parse().ok()?;
26183    if years < 0 || mons < 0 {
26184        return None;
26185    }
26186    let total = years.checked_mul(12)?.checked_add(mons)?;
26187    Some((if neg { -total } else { total }, 0, 0))
26188}
26189
26190/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26191/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26192fn parse_interval_clock(tok: &str) -> Option<i64> {
26193    let (neg, body) = match tok.strip_prefix('-') {
26194        Some(r) => (true, r),
26195        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26196    };
26197    let mut it = body.split(':');
26198    let h: i64 = it.next()?.parse().ok()?;
26199    let m: i64 = it.next()?.parse().ok()?;
26200    let s_tok = it.next().unwrap_or("0");
26201    if it.next().is_some() {
26202        return None;
26203    }
26204    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26205        let sec: i64 = sec.parse().ok()?;
26206        let mut f = alloc::string::String::from(frac);
26207        while f.len() < 6 {
26208            f.push('0');
26209        }
26210        f.truncate(6);
26211        let fus: i64 = f.parse().ok()?;
26212        sec.checked_mul(1_000_000)?.checked_add(fus)?
26213    } else {
26214        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26215    };
26216    let total = h
26217        .checked_mul(3_600_000_000)?
26218        .checked_add(m.checked_mul(60_000_000)?)?
26219        .checked_add(sec_us)?;
26220    Some(if neg { -total } else { total })
26221}
26222
26223/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26224/// every spelling PG accepts (measured against live PG18.4, not guessed):
26225/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26226/// Before this, the unit table matched long names only, with an ad-hoc
26227/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26228/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26229/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26230/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26231/// fractional) both read from this one table now.
26232fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26233    let u = raw.to_ascii_lowercase();
26234    Some(match u.as_str() {
26235        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26236            "microsecond"
26237        }
26238        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26239            "millisecond"
26240        }
26241        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26242        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26243        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26244        "day" | "days" | "d" => "day",
26245        "week" | "weeks" | "w" => "week",
26246        "month" | "months" | "mon" | "mons" => "month",
26247        "year" | "years" | "yr" | "yrs" | "y" => "year",
26248        "decade" | "decades" | "dec" | "decs" => "decade",
26249        "century" | "centuries" | "cent" | "c" => "century",
26250        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26251        _ => return None,
26252    })
26253}
26254
26255/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26256/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26258pub(crate) enum IntervalField {
26259    Year,
26260    Month,
26261    Day,
26262    Hour,
26263    Minute,
26264    Second,
26265}
26266
26267/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26268/// spellings aren't standard for the qualifier position, so only the singular
26269/// forms are accepted.
26270/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26271/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26272/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26273/// take a `'1 2'` style literal — are not read here; they stay a parse
26274/// error rather than being silently misread.)
26275/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26276///
26277/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26278/// to do with a `@@` engine setting, and an unset one reads NULL rather
26279/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26280/// were the same node and `SELECT @x` answered "Unknown system variable".)
26281/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26282/// not see a session override — measured, after `SET autocommit=0`,
26283/// `@@global.autocommit` is still 1.
26284///
26285/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26286/// the parser's nesting budget is tuned against, and building these
26287/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26288/// wall `parse_left_right_atom` and friends were factored out for).
26289#[inline(never)]
26290fn variable_ref_atom(raw: &str) -> Expr {
26291    let user_var = !raw.starts_with("@@");
26292    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26293    Expr::FunctionCall {
26294        name: String::from(if user_var {
26295            "__spg_user_var"
26296        } else {
26297            "__spg_session_var"
26298        }),
26299        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26300    }
26301}
26302
26303fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26304    let Token::Ident(s) = tok else { return None };
26305    Some(match () {
26306        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26307        () if s.eq_ignore_ascii_case("second") => "second",
26308        () if s.eq_ignore_ascii_case("minute") => "minute",
26309        () if s.eq_ignore_ascii_case("hour") => "hour",
26310        () if s.eq_ignore_ascii_case("day") => "day",
26311        () if s.eq_ignore_ascii_case("week") => "week",
26312        () if s.eq_ignore_ascii_case("month") => "month",
26313        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26314        () if s.eq_ignore_ascii_case("year") => "year",
26315        () => return None,
26316    })
26317}
26318
26319/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26320/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26321/// which constructs the value at run time. Only the slot the unit names
26322/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26323/// slot the builtin has (months and fractional seconds respectively).
26324fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26325    let zero = || Expr::Literal(Literal::Integer(0));
26326    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26327        lhs: alloc::boxed::Box::new(qty.clone()),
26328        op,
26329        rhs: alloc::boxed::Box::new(by),
26330    };
26331    // (years, months, weeks, days, hours, mins, secs)
26332    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26333    match unit {
26334        "year" => args[0] = qty,
26335        "quarter" => {
26336            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26337        }
26338        "month" => args[1] = qty,
26339        "week" => args[2] = qty,
26340        "day" => args[3] = qty,
26341        "hour" => args[4] = qty,
26342        "minute" => args[5] = qty,
26343        "second" => args[6] = qty,
26344        // The builtin's seconds slot takes a fraction, so microseconds ride
26345        // it scaled down; the divisor is a NUMERIC literal so the division
26346        // stays exact rather than going through a float.
26347        "microsecond" => {
26348            args[6] = scaled(
26349                crate::ast::BinOp::Div,
26350                Expr::Literal(Literal::Numeric {
26351                    unscaled: 1_000_000,
26352                    scale: 0,
26353                }),
26354            );
26355        }
26356        _ => args[3] = qty,
26357    }
26358    Expr::FunctionCall {
26359        name: alloc::string::String::from("make_interval"),
26360        args,
26361    }
26362}
26363
26364/// `(count, unit)` → `(months, days, micros)`.
26365fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26366    let n: i64 = count.trim().parse().ok()?;
26367    Some(match unit {
26368        "microsecond" => (0, 0, n),
26369        "second" => (0, 0, n.checked_mul(1_000_000)?),
26370        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26371        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26372        "day" => (0, i32::try_from(n).ok()?, 0),
26373        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26374        "month" => (i32::try_from(n).ok()?, 0, 0),
26375        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26376        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26377        _ => return None,
26378    })
26379}
26380
26381fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26382    let Token::Ident(s) = tok else { return None };
26383    Some(match () {
26384        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26385        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26386        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26387        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26388        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26389        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26390        () => return None,
26391    })
26392}
26393
26394/// v7.39 (read01 round 102) — interpret an interval literal under a field
26395/// qualifier. Returns `(months, days, micros)`.
26396///
26397/// * A single field applied to a bare number sets which unit the number means,
26398///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26399///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26400/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26401/// * Every other range, and any literal a single field can't read as a plain
26402///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26403///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26404///   like PG, and the qualifier there only bounds precision.
26405fn interpret_qualified_interval(
26406    text: &str,
26407    (f1, f2): (IntervalField, Option<IntervalField>),
26408) -> Option<(i32, i32, i64)> {
26409    if let Some(f2) = f2 {
26410        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26411            if let Some(m) = parse_year_month_literal(text) {
26412                return Some((m, 0, 0));
26413            }
26414        }
26415        return parse_interval_text(text);
26416    }
26417    // Single field: reinterpret a bare number; otherwise the default parse.
26418    let trimmed = text.trim();
26419    if let Ok(val) = trimmed.parse::<f64>() {
26420        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26421        #[allow(clippy::cast_possible_truncation)]
26422        let whole = val as i64;
26423        #[allow(clippy::cast_possible_truncation)]
26424        let secs_micros = {
26425            let m = val * 1_000_000.0;
26426            if m >= 0.0 {
26427                (m + 0.5) as i64
26428            } else {
26429                (m - 0.5) as i64
26430            }
26431        };
26432        return Some(match f1 {
26433            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26434            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26435            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26436            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26437            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26438            IntervalField::Second => (0, 0, secs_micros),
26439        });
26440    }
26441    parse_interval_text(text)
26442}
26443
26444/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26445fn parse_year_month_literal(text: &str) -> Option<i32> {
26446    let t = text.trim();
26447    let (neg, body) = match t.strip_prefix('-') {
26448        Some(r) => (true, r),
26449        None => (false, t.strip_prefix('+').unwrap_or(t)),
26450    };
26451    let mut it = body.split('-');
26452    let years: i32 = it.next()?.trim().parse().ok()?;
26453    let months: i32 = match it.next() {
26454        Some(m) => m.trim().parse().ok()?,
26455        None => 0,
26456    };
26457    if it.next().is_some() {
26458        return None;
26459    }
26460    let total = years.checked_mul(12)?.checked_add(months)?;
26461    Some(if neg { -total } else { total })
26462}
26463
26464pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26465    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26466    // `@` is decorative; a trailing `ago` negates the whole interval.
26467    let mut trimmed = s.trim();
26468    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26469    let mut negate = false;
26470    if let Some(rest) = trimmed
26471        .strip_suffix("ago")
26472        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26473    {
26474        negate = true;
26475        trimmed = rest.trim();
26476    }
26477    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26478        let (mo, d, us) = v?;
26479        if negate {
26480            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26481        } else {
26482            Some((mo, d, us))
26483        }
26484    };
26485    let s = trimmed;
26486    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26487    // are single tokens, not the `<n> <unit>` pair form handled below.
26488    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26489        return finish(parse_iso8601_interval(rest));
26490    }
26491    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26492        if let Some(iv) = parse_year_month_interval(trimmed) {
26493            return finish(Some(iv));
26494        }
26495    }
26496    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26497    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26498    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26499    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26500        if let Ok(n) = trimmed.parse::<i64>() {
26501            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26502        }
26503        if let Ok(f) = trimmed.parse::<f64>() {
26504            if f.is_finite() {
26505                #[allow(clippy::cast_possible_truncation)]
26506                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26507            }
26508        }
26509    }
26510    // v7.39 (round 243) — PG accepts the number and unit run together
26511    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26512    // the `<n> <unit>` pair loop below sees them as two.
26513    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26514    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26515    for p in raw_parts {
26516        let boundary = p
26517            .char_indices()
26518            .find(|(i, c)| {
26519                *i > 0
26520                    && c.is_ascii_alphabetic()
26521                    && p[..*i]
26522                        .chars()
26523                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26524                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26525            })
26526            .map(|(i, _)| i);
26527        match boundary {
26528            Some(i) => {
26529                parts.push(&p[..i]);
26530                parts.push(&p[i..]);
26531            }
26532            None => parts.push(p),
26533        }
26534    }
26535    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26536    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26537    // remains is the `<n> <unit>` pair form handled below.
26538    let mut clock_us: i64 = 0;
26539    let mut had_clock = false;
26540    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26541        clock_us = parse_interval_clock(parts[pos])?;
26542        parts.remove(pos);
26543        had_clock = true;
26544    }
26545    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26546    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26547    let mut lone_days: i32 = 0;
26548    if had_clock && parts.len() == 1 {
26549        if let Ok(n) = parts[0].parse::<i64>() {
26550            lone_days = i32::try_from(n).ok()?;
26551            parts.clear();
26552        }
26553    }
26554    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26555        return None;
26556    }
26557    let mut months: i32 = 0;
26558    let mut days: i32 = lone_days;
26559    let mut micros: i64 = clock_us;
26560    let mut i = 0;
26561    while i < parts.len() {
26562        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26563        if let Ok(n) = parts[i].parse::<i64>() {
26564            match unit_stripped {
26565                "microsecond" => micros = micros.checked_add(n)?,
26566                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26567                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26568                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26569                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26570                "day" => {
26571                    let n32 = i32::try_from(n).ok()?;
26572                    days = days.checked_add(n32)?;
26573                }
26574                "week" => {
26575                    let n32 = i32::try_from(n).ok()?;
26576                    days = days.checked_add(n32.checked_mul(7)?)?;
26577                }
26578                "month" => {
26579                    let n32 = i32::try_from(n).ok()?;
26580                    months = months.checked_add(n32)?;
26581                }
26582                "year" => {
26583                    let n32 = i32::try_from(n).ok()?;
26584                    months = months.checked_add(n32.checked_mul(12)?)?;
26585                }
26586                // v7.39 (read01 timestamp.c) — the larger calendar units.
26587                "decade" => {
26588                    let n32 = i32::try_from(n).ok()?;
26589                    months = months.checked_add(n32.checked_mul(120)?)?;
26590                }
26591                "century" => {
26592                    let n32 = i32::try_from(n).ok()?;
26593                    months = months.checked_add(n32.checked_mul(1200)?)?;
26594                }
26595                "millennium" => {
26596                    let n32 = i32::try_from(n).ok()?;
26597                    months = months.checked_add(n32.checked_mul(12000)?)?;
26598                }
26599                _ => return None,
26600            }
26601        } else if let Ok(f) = parts[i].parse::<f64>() {
26602            // Fractional units cascade down to the next-finer field the way
26603            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26604            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26605            // no_std: f64 has no trunc/fract/round methods, so do them with
26606            // casts (toward-zero) + explicit round-half-away-from-zero.
26607            #[allow(clippy::cast_possible_truncation)]
26608            fn round_i64(x: f64) -> i64 {
26609                if x >= 0.0 {
26610                    (x + 0.5) as i64
26611                } else {
26612                    (x - 0.5) as i64
26613                }
26614            }
26615            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26616            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26617                const DAY_US: f64 = 86_400_000_000.0;
26618                let whole = d as i64; // truncates toward zero
26619                let frac = d - whole as f64;
26620                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26621                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26622                Some(())
26623            }
26624            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26625            match unit_stripped {
26626                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26627                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26628                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26629                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26630                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26631                "day" => add_days_frac(&mut days, &mut micros, f)?,
26632                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26633                "month" => {
26634                    let whole = f as i64;
26635                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26636                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26637                }
26638                "year" => {
26639                    let m = f * 12.0;
26640                    let whole = m as i64;
26641                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26642                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26643                }
26644                _ => return None,
26645            }
26646        } else {
26647            return None;
26648        }
26649        i += 2;
26650    }
26651    finish(Some((months, days, micros)))
26652}
26653
26654/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26655/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26656/// `interval` is intentionally absent (handled by its own parser arm).
26657/// Returns `None` for names that aren't sensible as a bare typed literal, so
26658/// the caller falls back to treating the ident as a column reference.
26659fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26660    Some(match ident {
26661        "date" => CastTarget::Date,
26662        "timestamp" | "datetime" => CastTarget::Timestamp,
26663        "timestamptz" => CastTarget::Timestamptz,
26664        "bool" | "boolean" => CastTarget::Bool,
26665        "int" | "integer" | "int4" => CastTarget::Int,
26666        "bigint" | "int8" => CastTarget::BigInt,
26667        "float8" | "double precision" => CastTarget::Float,
26668        "uuid" => CastTarget::Uuid,
26669        "bytea" => CastTarget::Bytea,
26670        "json" => CastTarget::Json,
26671        "jsonb" => CastTarget::Jsonb,
26672        // Types without a dedicated CastTarget variant flow through the
26673        // generic Named path (engine resolves via column_type_to_data_type).
26674        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26675        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26676        | "money" | "bit" | "varbit"
26677        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26678        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26679        // Range / multirange types likewise.
26680        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26681        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26682        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26683        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26684        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26685            CastTarget::Named(alloc::string::String::from(ident))
26686        }
26687        _ => return None,
26688    })
26689}
26690
26691/// v7.12.4 — map a bare type-name identifier (the form that
26692/// appears in a function arg list or RETURNS clause) to a
26693/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26694/// types so the caller can preserve them as
26695/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26696///
26697/// Subset of the full column-type grammar — we deliberately
26698/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26699/// here because function-arg types in v7.12.4 are mostly the
26700/// bare form (`text`, `int`, `bytea`, …).
26701/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26702/// than being `name TYPE`?
26703///
26704/// The multi-word spellings SQL allows for a bare argument type, each
26705/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26706///
26707/// NOTE this list also exists in `spg-storage`, which computes the
26708/// signature key from the rendered argument text and has to reach the
26709/// same verdict. The two crates are siblings — neither depends on the
26710/// other — and each already carries its own table of type spellings
26711/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26712/// there), so this follows the structure rather than inventing new
26713/// duplication. Recorded as V49.
26714pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26715    let t = phrase.trim().to_ascii_lowercase();
26716    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26717    matches!(
26718        base,
26719        "double precision"
26720            | "character varying"
26721            | "bit varying"
26722            | "timestamp with time zone"
26723            | "timestamp without time zone"
26724            | "time with time zone"
26725            | "time without time zone"
26726            | "national character"
26727            | "national character varying"
26728    )
26729}
26730
26731fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26732    Some(match ident.to_ascii_lowercase().as_str() {
26733        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26734        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26735        "bigint" => ColumnTypeName::BigInt,
26736        "float" | "double" => ColumnTypeName::Float,
26737        // v7.39 (round 269) — real is 32-bit.
26738        "real" | "float4" => ColumnTypeName::Real,
26739        "text" => ColumnTypeName::Text,
26740        "bool" | "boolean" => ColumnTypeName::Bool,
26741        "date" => ColumnTypeName::Date,
26742        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26743        "timestamptz" => ColumnTypeName::Timestamptz,
26744        "json" => ColumnTypeName::Json,
26745        "jsonb" => ColumnTypeName::Jsonb,
26746        "bytea" | "bytes" => ColumnTypeName::Bytes,
26747        "tsvector" => ColumnTypeName::TsVector,
26748        "tsquery" => ColumnTypeName::TsQuery,
26749        "uuid" => ColumnTypeName::Uuid,
26750        "interval" => ColumnTypeName::Interval,
26751        "time" => ColumnTypeName::Time,
26752        "year" => ColumnTypeName::Year,
26753        "timetz" => ColumnTypeName::TimeTz,
26754        "money" => ColumnTypeName::Money,
26755        _ => return None,
26756    })
26757}
26758
26759/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26760/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26761///
26762/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26763/// / embedded SQL land in v7.12.5+):
26764///
26765/// ```text
26766///   body          := [ws] block [ws]
26767///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26768///   stmt          := assign | return
26769///   assign        := assign_target := expr
26770///   assign_target := ( NEW | OLD ) . ident | ident
26771///   return        := RETURN ( NEW | OLD | NULL | expr )
26772/// ```
26773///
26774/// `expr` is parsed by recursing into the regular `Parser` — so a
26775/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26776/// NEW.subject || ' ' || NEW.sender)` body shape works without
26777/// the body parser knowing what `to_tsvector` is.
26778///
26779/// Errors here cause the caller to fall back to
26780/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26781/// successful, but the executor will refuse to invoke the
26782/// function with an "unparseable body" error.
26783/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26784/// from the crate root as `spg_sql::parse_function_body`.
26785pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26786    parse_plpgsql_body(body)
26787}
26788
26789fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26790    // Use the regular lexer on the body text. The trailing
26791    // `END;` may or may not have a semicolon; the lexer treats
26792    // both forms identically.
26793    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26794        message: alloc::format!("plpgsql body lex error: {e}"),
26795        token_pos: 0,
26796    })?;
26797    let mut parser = Parser::new(tokens);
26798    parser.parse_plpgsql_block()
26799}
26800
26801/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26802fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26803    match v {
26804        crate::ast::SetValue::String(s)
26805        | crate::ast::SetValue::Ident(s)
26806        | crate::ast::SetValue::Number(s) => s.clone(),
26807        crate::ast::SetValue::Default => "DEFAULT".into(),
26808    }
26809}
26810
26811/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26812/// contains an aggregate call at ITS OWN query level (recursion stops at
26813/// sublink boundaries — a sublink's aggregates belong to the sublink).
26814/// Backs the "aggregate functions are not allowed in a recursive query's
26815/// recursive term" well-formedness check.
26816fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26817    const AGG_NAMES: &[&str] = &[
26818        "count",
26819        "sum",
26820        "min",
26821        "max",
26822        "avg",
26823        "string_agg",
26824        "array_agg",
26825        "bool_and",
26826        "bool_or",
26827        "every",
26828        "any_value",
26829        "json_agg",
26830        "jsonb_agg",
26831        "json_object_agg",
26832        "jsonb_object_agg",
26833        "bit_and",
26834        "bit_or",
26835        "bit_xor",
26836        "var_pop",
26837        "var_samp",
26838        "variance",
26839        "stddev",
26840        "stddev_pop",
26841        "stddev_samp",
26842        "range_agg",
26843        "range_intersect_agg",
26844        "percentile_cont",
26845        "percentile_disc",
26846        "mode",
26847        "corr",
26848        "covar_pop",
26849        "covar_samp",
26850    ];
26851    match e {
26852        Expr::AggregateOrdered { .. } => true,
26853        Expr::FunctionCall { name, args } => {
26854            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26855                || args.iter().any(expr_has_toplevel_aggregate)
26856        }
26857        Expr::NamedArg { expr, .. }
26858        | Expr::Variadic(expr)
26859        | Expr::Unary { expr, .. }
26860        | Expr::Cast { expr, .. }
26861        | Expr::IsNull { expr, .. }
26862        | Expr::FieldAccess { base: expr, .. }
26863        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26864        Expr::Binary { lhs, rhs, .. } => {
26865            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26866        }
26867        Expr::Like { expr, pattern, .. } => {
26868            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26869        }
26870        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26871        Expr::InList { expr, list, .. } => {
26872            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26873        }
26874        Expr::ArraySubscript { target, index } => {
26875            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26876        }
26877        Expr::ArraySlice { target, lo, hi } => {
26878            expr_has_toplevel_aggregate(target)
26879                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26880                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26881        }
26882        Expr::AnyAll { expr, array, .. } => {
26883            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26884        }
26885        Expr::Case {
26886            operand,
26887            branches,
26888            else_branch,
26889        } => {
26890            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26891                || branches
26892                    .iter()
26893                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26894                || else_branch
26895                    .as_deref()
26896                    .is_some_and(expr_has_toplevel_aggregate)
26897        }
26898        // The outer-level operands of a sublink can aggregate; the sublink's
26899        // own body cannot leak its aggregates up here.
26900        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26901        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26902            row.iter().any(expr_has_toplevel_aggregate)
26903        }
26904        _ => false,
26905    }
26906}
26907
26908/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26909/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26910/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26911/// sublink and is legal in a recursive term, so it is not walked here.
26912fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26913    let mut exprs: Vec<&Expr> = Vec::new();
26914    for it in &s.items {
26915        if let crate::ast::SelectItem::Expr { expr, .. } = it {
26916            exprs.push(expr);
26917        }
26918    }
26919    if let Some(w) = &s.where_ {
26920        exprs.push(w);
26921    }
26922    if let Some(h) = &s.having {
26923        exprs.push(h);
26924    }
26925    if let Some(g) = &s.group_by {
26926        exprs.extend(g.iter());
26927    }
26928    if let Some(from) = &s.from {
26929        for j in &from.joins {
26930            if let Some(on) = &j.on {
26931                exprs.push(on);
26932            }
26933        }
26934    }
26935    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26936}
26937
26938/// Does this expression contain a sublink whose subquery mentions `name`?
26939fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26940    match e {
26941        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26942        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26943        Expr::InSubquery { expr, subquery, .. } => {
26944            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26945        }
26946        Expr::RowInSubquery { row, subquery, .. } => {
26947            row.iter().any(|x| expr_sublink_mentions(x, name))
26948                || select_mentions_table(subquery, name)
26949        }
26950        Expr::RowCmpSubquery { row, subquery, .. } => {
26951            row.iter().any(|x| expr_sublink_mentions(x, name))
26952                || select_mentions_table(subquery, name)
26953        }
26954        Expr::NamedArg { expr, .. }
26955        | Expr::Variadic(expr)
26956        | Expr::Unary { expr, .. }
26957        | Expr::Cast { expr, .. }
26958        | Expr::IsNull { expr, .. }
26959        | Expr::FieldAccess { base: expr, .. }
26960        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26961        Expr::Binary { lhs, rhs, .. } => {
26962            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26963        }
26964        Expr::Like { expr, pattern, .. } => {
26965            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26966        }
26967        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26968            args.iter().any(|x| expr_sublink_mentions(x, name))
26969        }
26970        Expr::InList { expr, list, .. } => {
26971            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26972        }
26973        Expr::ArraySubscript { target, index } => {
26974            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26975        }
26976        Expr::ArraySlice { target, lo, hi } => {
26977            expr_sublink_mentions(target, name)
26978                || lo
26979                    .as_deref()
26980                    .is_some_and(|x| expr_sublink_mentions(x, name))
26981                || hi
26982                    .as_deref()
26983                    .is_some_and(|x| expr_sublink_mentions(x, name))
26984        }
26985        Expr::AnyAll { expr, array, .. } => {
26986            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26987        }
26988        Expr::Case {
26989            operand,
26990            branches,
26991            else_branch,
26992        } => {
26993            operand
26994                .as_deref()
26995                .is_some_and(|x| expr_sublink_mentions(x, name))
26996                || branches
26997                    .iter()
26998                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26999                || else_branch
27000                    .as_deref()
27001                    .is_some_and(|x| expr_sublink_mentions(x, name))
27002        }
27003        _ => false,
27004    }
27005}
27006
27007/// Does this SELECT (in full — FROM tables, derived tables, its own
27008/// sublinks, and union arms) mention the named table?
27009fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27010    if let Some(from) = &s.from {
27011        if from.primary.name.eq_ignore_ascii_case(name) {
27012            return true;
27013        }
27014        if let Some(sub) = &from.primary.lateral_subquery
27015            && select_mentions_table(sub, name)
27016        {
27017            return true;
27018        }
27019        for j in &from.joins {
27020            if j.table.name.eq_ignore_ascii_case(name) {
27021                return true;
27022            }
27023            if let Some(sub) = &j.table.lateral_subquery
27024                && select_mentions_table(sub, name)
27025            {
27026                return true;
27027            }
27028        }
27029    }
27030    if select_has_self_ref_in_sublink(s, name) {
27031        return true;
27032    }
27033    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27034}
27035
27036/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27037/// row count, the way PG evaluates one before applying it.
27038///
27039/// `None` = not a constant (a column, a subquery, a function call).
27040/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27041/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27042/// All wordings were read off live PG 18.4.
27043fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27044    use crate::ast::{BinOp, Expr, Literal, UnOp};
27045    match e {
27046        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27047        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27048            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27049        }
27050        // PG coerces a string by its CONTENT, and fails on the value.
27051        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27052            |_| {
27053                Err(alloc::format!(
27054                    "invalid input syntax for type bigint: \"{t}\""
27055                ))
27056            },
27057            |n| Ok(i128::from(n)),
27058        )),
27059        Expr::Literal(Literal::Bool(_)) => Some(Err(
27060            "argument of {L} must be type bigint, not type boolean".into(),
27061        )),
27062        Expr::Unary {
27063            op: UnOp::Neg,
27064            expr,
27065        } => match fold_limit_constant(expr)? {
27066            Ok(v) => Some(Ok(-v)),
27067            e @ Err(_) => Some(e),
27068        },
27069        Expr::Binary { lhs, op, rhs } => {
27070            let a = match fold_limit_constant(lhs)? {
27071                Ok(v) => v,
27072                e @ Err(_) => return Some(e),
27073            };
27074            let b = match fold_limit_constant(rhs)? {
27075                Ok(v) => v,
27076                e @ Err(_) => return Some(e),
27077            };
27078            let out = match op {
27079                BinOp::Add => a.checked_add(b),
27080                BinOp::Sub => a.checked_sub(b),
27081                BinOp::Mul => a.checked_mul(b),
27082                BinOp::Div if b != 0 => a.checked_div(b),
27083                BinOp::Div => return Some(Err("division by zero".into())),
27084                BinOp::Mod if b != 0 => a.checked_rem(b),
27085                BinOp::Mod => return Some(Err("division by zero".into())),
27086                _ => return None,
27087            };
27088            // PG evaluates the arithmetic in the operand's own type, so an
27089            // int-by-int product that leaves int range fails there — before
27090            // the row count is ever looked at.
27091            match out {
27092                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27093                    Some(Err("integer out of range".into()))
27094                }
27095                Some(v) => Some(Ok(v)),
27096                None => Some(Err("integer out of range".into())),
27097            }
27098        }
27099        _ => None,
27100    }
27101}
27102
27103/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27104/// cast, which is what makes `LIMIT 2.5` keep three rows.
27105fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27106    if scale == 0 {
27107        return unscaled;
27108    }
27109    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27110        return 0;
27111    };
27112    let neg = unscaled < 0;
27113    let mag = unscaled.unsigned_abs() as i128;
27114    let rounded = (mag + div / 2) / div;
27115    if neg { -rounded } else { rounded }
27116}
27117
27118#[cfg(test)]
27119mod tests {
27120    use super::*;
27121    use alloc::string::ToString;
27122
27123    fn parse(s: &str) -> Statement {
27124        parse_statement(s).expect("parse ok")
27125    }
27126
27127    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27128    // `tables`, `partition`, etc. are unreserved keywords per PG's
27129    // `pg_get_keywords()` and MUST be usable as column / table /
27130    // alias names. Pre-T4 every drop-in user whose schema had one
27131    // of these as a column name (sentori events.release, mailrs
27132    // messages.index in some forks) blew the parser up at CREATE
27133    // TABLE time with "expected identifier, got Release". The
27134    // generalisation lives in `unreserved_keyword_text` + the
27135    // `expect_ident_like` and `parse_atom` arms that consult it.
27136    #[test]
27137    fn release_usable_as_column_name_in_create_table() {
27138        let stmt =
27139            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27140        if let Statement::CreateTable(t) = stmt {
27141            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27142            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27143        } else {
27144            panic!("expected CreateTable");
27145        }
27146    }
27147
27148    #[test]
27149    fn release_usable_as_column_ref_in_select_projection() {
27150        // The sentori `0003_partition_events.sql` INSERT-SELECT
27151        // walk references `release` in both column lists; the
27152        // projection-side use exercises `parse_atom`'s relaxed
27153        // identifier set.
27154        parse("SELECT id, release, payload FROM events WHERE id = 1");
27155    }
27156
27157    #[test]
27158    fn release_usable_as_column_ref_in_insert_column_list() {
27159        // INSERT INTO t (id, release, payload) VALUES (…)
27160        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27161    }
27162
27163    #[test]
27164    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27165        // Sentori `0013_audit_tombstone.sql` issues
27166        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27167        // emits Token::Drop (not Ident("drop")); the parser must
27168        // accept both in the ALTER COLUMN sub-dispatch.
27169        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27170    }
27171
27172    #[test]
27173    fn create_index_accepts_parenthesised_expression_key() {
27174        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27175        // expression index. Pre-T4 the parser bailed at the
27176        // inner `(` with "expected column ident or expression,
27177        // got LParen". The Token::LParen arm in CREATE INDEX
27178        // routes through the expression parser instead.
27179        parse(
27180            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27181             ON events ((payload->'bundle'->>'id'))",
27182        );
27183    }
27184
27185    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27186    // surface as parse errors, never stack overflows (embed hosts
27187    // abort on overflow).
27188    /// The nesting budget is a COUNT; what it has to fit inside is a
27189    /// number of BYTES, and only one of those two is stable across
27190    /// compiler versions. Round 847 measured 30,336 bytes per level
27191    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27192    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27193    /// aborted instead of erroring, which is precisely the outcome it
27194    /// exists to rule out.
27195    ///
27196    /// So the budget is metered rather than assumed. The ceiling leaves
27197    /// the depth SPG advertises fitting in a default 2 MiB thread with
27198    /// room to spare, in the debug build, where frames are widest.
27199    #[test]
27200    fn nesting_frame_cost_stays_under_ceiling() {
27201        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27202        // thread keeps a margin for whatever called the parser.
27203        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27204
27205        frame_meter::reset();
27206        let depth = frame_meter::SAMPLE_HI + 8;
27207        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27208        parse(&sql);
27209
27210        let per_level = frame_meter::bytes_per_level();
27211        {
27212            extern crate std;
27213            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27214        }
27215        assert!(
27216            per_level <= CEILING,
27217            "{per_level} bytes per nesting level exceeds {CEILING}; \
27218             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27219             in parse_expr_inner / parse_unary rather than lowering the \
27220             depth or widening the stack.",
27221            per_level * MAX_NEST_DEPTH
27222        );
27223    }
27224
27225    #[test]
27226    fn nesting_budget_errors_cleanly() {
27227        let depth = MAX_NEST_DEPTH + 50;
27228        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27229        let err = parse_statement(&sql).expect_err("must reject");
27230        assert!(err.message.contains("nests deeper"), "{err:?}");
27231        // Within budget still parses.
27232        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27233        parse(&sql);
27234    }
27235
27236    #[test]
27237    fn binary_chain_budget_errors_cleanly() {
27238        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27239        let err = parse_statement(&sql).expect_err("must reject");
27240        assert!(err.message.contains("chained binary"), "{err:?}");
27241        // Within budget still parses (chain depth ≤ budget is safe
27242        // for recursive eval/drop on 2 MiB stacks).
27243        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27244        parse(&sql);
27245    }
27246
27247    #[test]
27248    fn in_list_unaffected_by_chain_budget() {
27249        // Flat InList: 20k elements parse fine and stay flat.
27250        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27251        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27252        let Statement::Select(s) = parse(&sql) else {
27253            panic!("expected select")
27254        };
27255        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27256            panic!("expected flat InList, got {:?}", s.where_)
27257        };
27258        assert_eq!(list.len(), 20_000);
27259        assert!(!negated);
27260    }
27261
27262    fn lit_int(n: i64) -> Expr {
27263        Expr::Literal(Literal::Integer(n))
27264    }
27265
27266    fn col(name: &str) -> Expr {
27267        Expr::Column(ColumnName {
27268            qualifier: None,
27269            name: name.into(),
27270        })
27271    }
27272
27273    #[test]
27274    fn select_single_integer() {
27275        let s = parse("SELECT 1");
27276        let Statement::Select(s) = s else {
27277            panic!("expected SELECT")
27278        };
27279        assert_eq!(s.items.len(), 1);
27280        assert!(s.from.is_none());
27281        assert!(s.where_.is_none());
27282    }
27283
27284    #[test]
27285    fn select_multiple_literal_kinds() {
27286        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27287        let Statement::Select(s) = s else {
27288            panic!("expected SELECT")
27289        };
27290        assert_eq!(s.items.len(), 5);
27291    }
27292
27293    #[test]
27294    fn select_wildcard_from_table() {
27295        let s = parse("SELECT * FROM users");
27296        let Statement::Select(s) = s else {
27297            panic!("expected SELECT")
27298        };
27299        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27300        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27301    }
27302
27303    #[test]
27304    fn select_with_table_alias() {
27305        let s = parse("SELECT * FROM users AS u");
27306        let Statement::Select(s) = s else {
27307            panic!("expected SELECT")
27308        };
27309        let t = &s.from.as_ref().unwrap().primary;
27310        assert_eq!(t.name, "users");
27311        assert_eq!(t.alias.as_deref(), Some("u"));
27312    }
27313
27314    #[test]
27315    fn select_with_where_eq() {
27316        let s = parse("SELECT a FROM t WHERE a = 1");
27317        let Statement::Select(s) = s else {
27318            panic!("expected SELECT")
27319        };
27320        let w = s.where_.unwrap();
27321        assert_eq!(
27322            w,
27323            Expr::Binary {
27324                lhs: Box::new(col("a")),
27325                op: BinOp::Eq,
27326                rhs: Box::new(lit_int(1)),
27327            }
27328        );
27329    }
27330
27331    #[test]
27332    fn arithmetic_precedence() {
27333        let s = parse("SELECT 1 + 2 * 3");
27334        let Statement::Select(s) = s else {
27335            panic!("expected SELECT")
27336        };
27337        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27338            panic!("wildcard?")
27339        };
27340        assert_eq!(
27341            expr,
27342            &Expr::Binary {
27343                lhs: Box::new(lit_int(1)),
27344                op: BinOp::Add,
27345                rhs: Box::new(Expr::Binary {
27346                    lhs: Box::new(lit_int(2)),
27347                    op: BinOp::Mul,
27348                    rhs: Box::new(lit_int(3)),
27349                }),
27350            }
27351        );
27352    }
27353
27354    #[test]
27355    fn parentheses_override_precedence() {
27356        let s = parse("SELECT (1 + 2) * 3");
27357        let Statement::Select(s) = s else {
27358            panic!("expected SELECT")
27359        };
27360        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27361            panic!()
27362        };
27363        assert_eq!(
27364            expr,
27365            &Expr::Binary {
27366                lhs: Box::new(Expr::Binary {
27367                    lhs: Box::new(lit_int(1)),
27368                    op: BinOp::Add,
27369                    rhs: Box::new(lit_int(2)),
27370                }),
27371                op: BinOp::Mul,
27372                rhs: Box::new(lit_int(3)),
27373            }
27374        );
27375    }
27376
27377    #[test]
27378    fn not_binds_below_comparison() {
27379        // `NOT a = 1` should parse as `NOT (a = 1)`.
27380        let s = parse("SELECT NOT a = 1 FROM t");
27381        let Statement::Select(s) = s else {
27382            panic!("expected SELECT")
27383        };
27384        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27385            panic!()
27386        };
27387        assert_eq!(
27388            expr,
27389            &Expr::Unary {
27390                op: UnOp::Not,
27391                expr: Box::new(Expr::Binary {
27392                    lhs: Box::new(col("a")),
27393                    op: BinOp::Eq,
27394                    rhs: Box::new(lit_int(1)),
27395                }),
27396            }
27397        );
27398    }
27399
27400    #[test]
27401    fn unary_minus_binds_above_multiplication() {
27402        // `-a * 2` should be `(-a) * 2`.
27403        let s = parse("SELECT -a * 2 FROM t");
27404        let Statement::Select(s) = s else {
27405            panic!("expected SELECT")
27406        };
27407        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27408            panic!()
27409        };
27410        assert_eq!(
27411            expr,
27412            &Expr::Binary {
27413                lhs: Box::new(Expr::Unary {
27414                    op: UnOp::Neg,
27415                    expr: Box::new(col("a")),
27416                }),
27417                op: BinOp::Mul,
27418                rhs: Box::new(lit_int(2)),
27419            }
27420        );
27421    }
27422
27423    #[test]
27424    fn qualified_column() {
27425        let s = parse("SELECT t.col FROM t");
27426        let Statement::Select(s) = s else {
27427            panic!("expected SELECT")
27428        };
27429        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27430            panic!()
27431        };
27432        assert_eq!(
27433            expr,
27434            &Expr::Column(ColumnName {
27435                qualifier: Some("t".into()),
27436                name: "col".into()
27437            })
27438        );
27439    }
27440
27441    #[test]
27442    fn select_item_alias_with_as() {
27443        let s = parse("SELECT a AS y FROM t");
27444        let Statement::Select(s) = s else {
27445            panic!("expected SELECT")
27446        };
27447        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27448            panic!()
27449        };
27450        assert_eq!(alias.as_deref(), Some("y"));
27451    }
27452
27453    #[test]
27454    fn trailing_semicolon_accepted() {
27455        let s = parse("SELECT 1;");
27456        let Statement::Select(s) = s else {
27457            panic!("expected SELECT")
27458        };
27459        assert_eq!(s.items.len(), 1);
27460    }
27461
27462    #[test]
27463    fn boolean_chain_with_and_or_not() {
27464        // (NOT a) OR (b AND (NOT c))
27465        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27466        let Statement::Select(s) = s else {
27467            panic!("expected SELECT")
27468        };
27469        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27470            panic!()
27471        };
27472        let expected = Expr::Binary {
27473            lhs: Box::new(Expr::Unary {
27474                op: UnOp::Not,
27475                expr: Box::new(col("a")),
27476            }),
27477            op: BinOp::Or,
27478            rhs: Box::new(Expr::Binary {
27479                lhs: Box::new(col("b")),
27480                op: BinOp::And,
27481                rhs: Box::new(Expr::Unary {
27482                    op: UnOp::Not,
27483                    expr: Box::new(col("c")),
27484                }),
27485            }),
27486        };
27487        assert_eq!(expr, &expected);
27488    }
27489
27490    #[test]
27491    fn empty_input_errors() {
27492        // v7.14.0 — pg_dump preambles emit several comment-only
27493        // / blank-line statements that collapse to Statement::
27494        // Empty rather than a parse error. The old "SELECT in
27495        // message" assertion is stale; verify the new contract:
27496        // empty / whitespace / comment-only input parses to
27497        // Statement::Empty.
27498        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27499        assert!(matches!(
27500            parse_statement("  \n\t ").unwrap(),
27501            Statement::Empty
27502        ));
27503        // Sanity: malformed-but-non-empty still errors.
27504        assert!(parse_statement("SELECT FROM WHERE").is_err());
27505    }
27506
27507    #[test]
27508    fn unmatched_paren_errors() {
27509        assert!(parse_statement("SELECT (1 + 2").is_err());
27510    }
27511
27512    #[test]
27513    fn display_round_trip_simple_select() {
27514        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27515        let text = original.to_string();
27516        let again = parse_statement(&text).expect("re-parse");
27517        assert_eq!(original, again);
27518    }
27519
27520    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27521
27522    #[test]
27523    fn create_table_single_column() {
27524        let s = parse("CREATE TABLE foo (a INT)");
27525        let Statement::CreateTable(c) = s else {
27526            panic!("expected CreateTable")
27527        };
27528        assert_eq!(c.name, "foo");
27529        assert_eq!(c.columns.len(), 1);
27530        assert_eq!(c.columns[0].name, "a");
27531        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27532        assert!(c.columns[0].nullable);
27533    }
27534
27535    #[test]
27536    fn create_table_multi_column_with_not_null_mix() {
27537        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27538        let Statement::CreateTable(c) = s else {
27539            panic!()
27540        };
27541        assert_eq!(c.columns.len(), 4);
27542        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27543        assert!(!c.columns[0].nullable);
27544        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27545        assert!(c.columns[1].nullable);
27546        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27547        assert!(!c.columns[2].nullable);
27548        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27549    }
27550
27551    #[test]
27552    fn create_table_bigint_supported() {
27553        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27554        let Statement::CreateTable(c) = s else {
27555            panic!()
27556        };
27557        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27558    }
27559
27560    #[test]
27561    fn create_table_vector_default_is_f32() {
27562        let s = parse("CREATE TABLE t (v VECTOR(128))");
27563        let Statement::CreateTable(c) = s else {
27564            panic!()
27565        };
27566        assert_eq!(
27567            c.columns[0].ty,
27568            ColumnTypeName::Vector {
27569                dim: 128,
27570                encoding: VecEncoding::F32,
27571            },
27572        );
27573    }
27574
27575    #[test]
27576    fn create_table_vector_using_sq8() {
27577        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27578        // Case-insensitive on both `USING` and the encoding name.
27579        for sql in [
27580            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27581            "CREATE TABLE t (v VECTOR(128) using sq8)",
27582        ] {
27583            let s = parse(sql);
27584            let Statement::CreateTable(c) = s else {
27585                panic!()
27586            };
27587            assert_eq!(
27588                c.columns[0].ty,
27589                ColumnTypeName::Vector {
27590                    dim: 128,
27591                    encoding: VecEncoding::Sq8,
27592                },
27593                "{sql}",
27594            );
27595        }
27596    }
27597
27598    #[test]
27599    fn create_table_vector_using_unknown_errors() {
27600        // v7.16.1 — the inline `USING <encoding>` shape on
27601        // CREATE TABLE column defs was withdrawn before
27602        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27603        // (col vector_<metric>_ops)`; the parser now rejects
27604        // USING at column-list position with a clearer
27605        // "expected ',' or ')'" message. Test asserts the
27606        // current rejection, not the old "unknown vector
27607        // encoding" string.
27608        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27609        assert!(
27610            err.message.contains("USING")
27611                || err.message.contains("using")
27612                || err.message.contains("')'")
27613                || err.message.contains("','"),
27614            "expected USING/column-list rejection, got: {}",
27615            err.message
27616        );
27617    }
27618
27619    #[test]
27620    fn vector_using_sq8_display_roundtrips() {
27621        // The Display impl must produce text that re-parses to the
27622        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27623        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27624        let Statement::CreateTable(c) = s else {
27625            panic!()
27626        };
27627        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27628    }
27629
27630    #[test]
27631    fn parser_recognises_placeholders() {
27632        use crate::ast::{Expr, SelectItem, Statement};
27633        // $N in expression position parses as Expr::Placeholder(N).
27634        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27635        let Statement::Select(sel) = s else { panic!() };
27636        assert!(matches!(
27637            sel.items[0],
27638            SelectItem::Expr {
27639                expr: Expr::Placeholder(1),
27640                alias: None
27641            }
27642        ));
27643        // $2 + 1
27644        let SelectItem::Expr {
27645            expr: Expr::Binary { lhs, rhs, .. },
27646            ..
27647        } = &sel.items[1]
27648        else {
27649            panic!()
27650        };
27651        assert!(matches!(**lhs, Expr::Placeholder(2)));
27652        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27653        // WHERE x = $3
27654        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27655            panic!()
27656        };
27657        assert!(matches!(**rhs, Expr::Placeholder(3)));
27658    }
27659
27660    #[test]
27661    fn parser_rejects_dollar_zero() {
27662        // $0 is not valid in PG; the lexer rejects it.
27663        assert!(parse_statement("SELECT $0").is_err());
27664    }
27665
27666    #[test]
27667    fn placeholder_display_roundtrips() {
27668        // The Display impl must produce text that re-lexes to the
27669        // same Placeholder token.
27670        let s = parse("SELECT $42 FROM t");
27671        let printed = s.to_string();
27672        assert!(printed.contains("$42"));
27673        let again = parse(&printed);
27674        assert_eq!(s, again);
27675    }
27676
27677    #[test]
27678    fn alter_index_rebuild_bare() {
27679        use crate::ast::{AlterIndexTarget, Statement};
27680        let s = parse("ALTER INDEX my_idx REBUILD");
27681        let Statement::AlterIndex(a) = s else {
27682            panic!("expected AlterIndex, got {s:?}")
27683        };
27684        assert_eq!(a.name, "my_idx");
27685        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27686    }
27687
27688    #[test]
27689    fn alter_index_rebuild_with_encoding() {
27690        use crate::ast::{AlterIndexTarget, Statement};
27691        for (sql, want) in [
27692            (
27693                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27694                VecEncoding::F32,
27695            ),
27696            (
27697                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27698                VecEncoding::Sq8,
27699            ),
27700            (
27701                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27702                VecEncoding::F16,
27703            ),
27704        ] {
27705            let s = parse(sql);
27706            let Statement::AlterIndex(a) = s else {
27707                panic!("{sql}: expected AlterIndex")
27708            };
27709            assert_eq!(a.name, "my_idx");
27710            assert_eq!(
27711                a.target,
27712                AlterIndexTarget::Rebuild {
27713                    encoding: Some(want)
27714                },
27715                "{sql}"
27716            );
27717        }
27718    }
27719
27720    #[test]
27721    fn alter_index_rebuild_unknown_encoding_errors() {
27722        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27723        assert!(
27724            err.message.contains("unknown vector encoding"),
27725            "got: {}",
27726            err.message
27727        );
27728    }
27729
27730    #[test]
27731    fn alter_index_rebuild_display_roundtrips() {
27732        for (input, want) in [
27733            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27734            (
27735                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27736                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27737            ),
27738            (
27739                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27740                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27741            ),
27742        ] {
27743            let s = parse(input);
27744            assert_eq!(s.to_string(), want);
27745        }
27746    }
27747
27748    #[test]
27749    fn create_table_unknown_type_defers_to_engine() {
27750        // v4.9 picked XML as a parse-time "unsupported column
27751        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27752        // an unknown type ident parses as Text + `user_type_ref`
27753        // so CREATE TABLE can resolve user-defined enum / domain
27754        // types — rejection of truly-unknown types moved to the
27755        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27756        // to a first-class built-in, so this probe switched to a
27757        // synthetic name nothing in the lexer will ever recognise.
27758        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27759        let Statement::CreateTable(t) = stmt else {
27760            panic!("expected CreateTable");
27761        };
27762        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27763    }
27764
27765    #[test]
27766    fn create_table_missing_table_keyword_errors() {
27767        assert!(parse_statement("CREATE x (a INT)").is_err());
27768    }
27769
27770    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27771    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27772
27773    #[test]
27774    fn parse_create_table_partition_by_range() {
27775        use crate::ast::{PartitionBySpec, PartitionKindAst};
27776        let stmt = parse_statement(
27777            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27778             payload JSONB) PARTITION BY RANGE (ts)",
27779        )
27780        .unwrap();
27781        let Statement::CreateTable(t) = stmt else {
27782            panic!("expected CreateTable");
27783        };
27784        assert!(t.partition_of.is_none(), "parent has no partition_of");
27785        assert_eq!(t.columns.len(), 3);
27786        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27787        assert_eq!(
27788            by,
27789            &PartitionBySpec {
27790                kind: PartitionKindAst::Range,
27791                key_columns: alloc::vec!["ts".to_string()],
27792            }
27793        );
27794        // Display round-trip preserves the suffix. `quote_ident`
27795        // only adds double quotes when the ident needs escaping, so
27796        // a plain `ts` survives bare here.
27797        assert!(
27798            t.to_string().contains("PARTITION BY RANGE (ts)"),
27799            "Display lost PARTITION BY suffix: {t}"
27800        );
27801    }
27802
27803    #[test]
27804    fn parse_create_table_partition_of_range() {
27805        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27806        let stmt = parse_statement(
27807            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27808             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27809        )
27810        .unwrap();
27811        let Statement::CreateTable(t) = stmt else {
27812            panic!("expected CreateTable");
27813        };
27814        assert!(t.columns.is_empty(), "child inherits columns from parent");
27815        assert!(t.partition_by.is_none());
27816        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27817        assert_eq!(of.parent_name, "events_partitioned");
27818        let PartitionOfSpec { bounds, .. } = of.clone();
27819        match bounds {
27820            PartitionOfBoundsAst::Range { lower, upper } => {
27821                assert!(lower.to_string().contains("2026-06-01"));
27822                assert!(upper.to_string().contains("2026-07-01"));
27823            }
27824            other => panic!("expected Range, got {other:?}"),
27825        }
27826        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27827        // skips quotes when not required, so the parent name appears
27828        // bare here.
27829        let s = t.to_string();
27830        assert!(
27831            s.contains("PARTITION OF events_partitioned"),
27832            "Display lost PARTITION OF: {s}"
27833        );
27834        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27835        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27836    }
27837
27838    #[test]
27839    fn parse_create_table_partition_of_default() {
27840        use crate::ast::PartitionOfBoundsAst;
27841        let stmt =
27842            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27843                .unwrap();
27844        let Statement::CreateTable(t) = stmt else {
27845            panic!("expected CreateTable");
27846        };
27847        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27848        assert_eq!(of.parent_name, "events_partitioned");
27849        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27850        assert!(
27851            t.to_string()
27852                .contains("PARTITION OF events_partitioned DEFAULT"),
27853            "Display lost DEFAULT: {t}"
27854        );
27855    }
27856
27857    #[test]
27858    fn parse_create_table_partition_by_list() {
27859        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27860        // child with `FOR VALUES IN (lit, lit, …)`.
27861        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27862        let parent =
27863            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27864                .unwrap();
27865        let Statement::CreateTable(t) = parent else {
27866            panic!("expected CreateTable");
27867        };
27868        let Some(PartitionBySpec {
27869            kind,
27870            ref key_columns,
27871        }) = t.partition_by
27872        else {
27873            panic!("expected PARTITION BY");
27874        };
27875        assert_eq!(kind, PartitionKindAst::List);
27876        assert_eq!(*key_columns, vec!["region".to_string()]);
27877        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27878
27879        let child = parse_statement(
27880            "CREATE TABLE events_apac PARTITION OF events_listed \
27881             FOR VALUES IN ('jp', 'kr', 'tw')",
27882        )
27883        .unwrap();
27884        let Statement::CreateTable(c) = child else {
27885            panic!("expected CreateTable");
27886        };
27887        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27888        let PartitionOfBoundsAst::List { values } = &of.bounds else {
27889            panic!("expected List bounds, got {:?}", of.bounds);
27890        };
27891        assert_eq!(values.len(), 3);
27892        let disp = c.to_string();
27893        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27894    }
27895
27896    #[test]
27897    fn parse_create_table_partition_by_hash() {
27898        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27899        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27900        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27901        let parent =
27902            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27903        let Statement::CreateTable(t) = parent else {
27904            panic!("expected CreateTable");
27905        };
27906        let Some(PartitionBySpec {
27907            kind,
27908            ref key_columns,
27909        }) = t.partition_by
27910        else {
27911            panic!("expected PARTITION BY");
27912        };
27913        assert_eq!(kind, PartitionKindAst::Hash);
27914        assert_eq!(*key_columns, vec!["id".to_string()]);
27915        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27916
27917        let child = parse_statement(
27918            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27919             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27920        )
27921        .unwrap();
27922        let Statement::CreateTable(c) = child else {
27923            panic!("expected CreateTable");
27924        };
27925        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27926        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27927            panic!("expected Hash bounds");
27928        };
27929        assert_eq!(modulus, 4);
27930        assert_eq!(remainder, 0);
27931        let disp = c.to_string();
27932        assert!(
27933            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27934            "Display lost HASH bounds: {disp}"
27935        );
27936
27937        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27938        let bad = parse_statement(
27939            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27940             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27941        );
27942        let msg = format!("{}", bad.unwrap_err());
27943        assert!(
27944            msg.contains("REMAINDER") && msg.contains("MODULUS"),
27945            "expected REMAINDER/MODULUS validation error: {msg}"
27946        );
27947    }
27948
27949    #[test]
27950    fn parse_create_table_partition_of_rejects_columns() {
27951        // v7.37.6-B contract: PARTITION OF children inherit columns
27952        // from the parent; an explicit list MUST surface as a parse
27953        // error rather than getting silently ignored.
27954        let err = parse_statement(
27955            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27956             FOR VALUES FROM ('a') TO ('b')",
27957        );
27958        assert!(err.is_err(), "expected parse error for explicit columns");
27959        let msg = format!("{}", err.unwrap_err());
27960        assert!(
27961            msg.contains("PARTITION OF") && msg.contains("column"),
27962            "error should mention PARTITION OF + columns: {msg}"
27963        );
27964    }
27965
27966    #[test]
27967    fn insert_single_value() {
27968        let s = parse("INSERT INTO foo VALUES (42)");
27969        let Statement::Insert(i) = s else {
27970            panic!("expected Insert")
27971        };
27972        assert_eq!(i.table, "foo");
27973        assert_eq!(i.rows.len(), 1);
27974        assert_eq!(i.rows[0].len(), 1);
27975        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27976    }
27977
27978    #[test]
27979    fn insert_multi_value_with_mixed_literals() {
27980        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27981        let Statement::Insert(i) = s else { panic!() };
27982        assert_eq!(i.rows.len(), 1);
27983        assert_eq!(i.rows[0].len(), 5);
27984    }
27985
27986    #[test]
27987    fn insert_missing_into_errors() {
27988        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27989    }
27990
27991    #[test]
27992    fn create_table_round_trip() {
27993        let original =
27994            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27995        let text = original.to_string();
27996        let again = parse_statement(&text).expect("re-parse");
27997        assert_eq!(original, again);
27998    }
27999
28000    #[test]
28001    fn insert_round_trip_with_negation_and_string() {
28002        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28003        let text = original.to_string();
28004        let again = parse_statement(&text).expect("re-parse");
28005        assert_eq!(original, again);
28006    }
28007
28008    #[test]
28009    fn unknown_keyword_at_statement_start_errors() {
28010        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28011        // the top-level dispatch still has no branch to take.
28012        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28013        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28014    }
28015
28016    // --- v0.8 CREATE INDEX --------------------------------------------------
28017
28018    #[test]
28019    fn create_index_basic() {
28020        let s = parse("CREATE INDEX idx_id ON users (id)");
28021        let Statement::CreateIndex(c) = s else {
28022            panic!("expected CreateIndex")
28023        };
28024        assert_eq!(c.name, "idx_id");
28025        assert_eq!(c.table, "users");
28026        assert_eq!(c.column, "id");
28027    }
28028
28029    #[test]
28030    fn create_index_missing_on_errors() {
28031        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28032    }
28033
28034    #[test]
28035    fn create_index_missing_paren_errors() {
28036        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28037    }
28038
28039    #[test]
28040    fn create_index_round_trip() {
28041        let original = parse("CREATE INDEX by_name ON users (name)");
28042        let again = parse_statement(&original.to_string()).unwrap();
28043        assert_eq!(original, again);
28044    }
28045
28046    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28047
28048    #[test]
28049    fn create_unique_index_basic() {
28050        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28051        let Statement::CreateIndex(c) = s else {
28052            panic!("expected CreateIndex");
28053        };
28054        assert!(c.is_unique);
28055        assert_eq!(c.column, "a");
28056        assert!(c.partial_predicate.is_none());
28057    }
28058
28059    #[test]
28060    fn create_unique_index_partial() {
28061        // mailrs's email_templates "one default per user" shape.
28062        let s = parse(
28063            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28064             ON email_templates (user_address) WHERE is_default = true",
28065        );
28066        let Statement::CreateIndex(c) = s else {
28067            panic!("expected CreateIndex");
28068        };
28069        assert!(c.is_unique);
28070        assert_eq!(c.table, "email_templates");
28071        assert_eq!(c.column, "user_address");
28072        assert!(c.partial_predicate.is_some());
28073    }
28074
28075    #[test]
28076    fn create_unique_index_composite_with_predicate() {
28077        // mailrs's calendar_events instance: composite columns.
28078        let s = parse(
28079            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28080             ON calendar_events (calendar_id, uid, recurrence_id) \
28081             WHERE recurrence_id IS NOT NULL",
28082        );
28083        let Statement::CreateIndex(c) = s else {
28084            panic!("expected CreateIndex");
28085        };
28086        assert!(c.is_unique);
28087        assert_eq!(c.column, "calendar_id");
28088        assert_eq!(
28089            c.extra_columns,
28090            vec!["uid".to_string(), "recurrence_id".to_string()]
28091        );
28092        assert!(c.partial_predicate.is_some());
28093    }
28094
28095    #[test]
28096    fn create_unique_index_using_btree_ok() {
28097        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28098        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28099    }
28100
28101    #[test]
28102    fn create_unique_index_using_hnsw_rejected() {
28103        let err =
28104            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28105        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28106    }
28107
28108    #[test]
28109    fn create_unique_index_round_trip() {
28110        let original = parse(
28111            "CREATE UNIQUE INDEX uq_calendar_events_master \
28112             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28113        );
28114        let again = parse_statement(&original.to_string()).unwrap();
28115        assert_eq!(original, again);
28116    }
28117
28118    #[test]
28119    fn create_unique_without_index_errors() {
28120        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28121        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28122        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28123    }
28124
28125    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28126
28127    #[test]
28128    fn create_table_bytea_column() {
28129        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28130        let Statement::CreateTable(c) = s else {
28131            panic!("expected CreateTable");
28132        };
28133        assert_eq!(c.columns.len(), 2);
28134        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28135        assert!(!c.columns[1].nullable);
28136    }
28137
28138    #[test]
28139    fn create_table_bytes_alias_column() {
28140        let s = parse("CREATE TABLE t (blob BYTES)");
28141        let Statement::CreateTable(c) = s else {
28142            panic!("expected CreateTable");
28143        };
28144        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28145    }
28146
28147    #[test]
28148    fn bytea_round_trip_display() {
28149        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28150        let again = parse_statement(&original.to_string()).unwrap();
28151        assert_eq!(original, again);
28152    }
28153
28154    // --- v0.9 transactions -------------------------------------------------
28155
28156    #[test]
28157    fn begin_commit_rollback_parse_as_unit_variants() {
28158        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28159        assert_eq!(parse("COMMIT"), Statement::Commit);
28160        // r1066 — PG synonyms pgbench's tpcb script relies on.
28161        assert_eq!(parse("END"), Statement::Commit);
28162        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28163        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28164        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28165        // Trailing semicolons accepted too.
28166        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28167        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28168        // statement (with or without the WORK/TRANSACTION noise word).
28169        assert_eq!(
28170            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28171            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28172        );
28173        assert_eq!(
28174            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28175            Statement::Begin(Some(IsolationLevel::Serializable))
28176        );
28177        // A non-isolation mode keeps the session default (None).
28178        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28179    }
28180
28181    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28182
28183    #[test]
28184    fn inner_product_binop_parses() {
28185        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28186        let Statement::Select(s) = s else { panic!() };
28187        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28188            panic!()
28189        };
28190        assert!(matches!(
28191            expr,
28192            Expr::Binary {
28193                op: BinOp::InnerProduct,
28194                ..
28195            }
28196        ));
28197    }
28198
28199    #[test]
28200    fn cosine_distance_binop_parses() {
28201        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28202        let Statement::Select(s) = s else { panic!() };
28203        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28204            panic!()
28205        };
28206        assert!(matches!(
28207            expr,
28208            Expr::Binary {
28209                op: BinOp::CosineDistance,
28210                ..
28211            }
28212        ));
28213    }
28214
28215    #[test]
28216    fn vector_cast_postfix_wraps_string_literal() {
28217        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28218        let Statement::Select(s) = s else { panic!() };
28219        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28220            panic!()
28221        };
28222        assert!(matches!(
28223            expr,
28224            Expr::Cast {
28225                target: CastTarget::Vector,
28226                ..
28227            }
28228        ));
28229    }
28230
28231    #[test]
28232    fn unsupported_cast_target_errors() {
28233        // v7.37.5 ship triage promoted the parser to accept every
28234        // ident as a `CastTarget::Named(canonical)`; the engine
28235        // surfaces the "unsupported cast target" error at eval
28236        // time when `type_name_to_data_type` can't resolve it.
28237        // Parser-side error now requires a NON-ident after `::`
28238        // (e.g. a punctuation token).
28239        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28240        assert_eq!(err.message, "syntax error at or near \",\"");
28241    }
28242
28243    #[test]
28244    fn tx_statements_round_trip() {
28245        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28246            let original = parse(q);
28247            let again = parse_statement(&original.to_string()).unwrap();
28248            assert_eq!(original, again);
28249        }
28250    }
28251
28252    #[test]
28253    fn interval_text_parsing_units() {
28254        // v7.37.5 β — three-field shape `(months, days, micros)` so
28255        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28256        // Single unit.
28257        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28258        assert_eq!(
28259            parse_interval_text("24 hours"),
28260            Some((0, 0, 86_400_000_000))
28261        );
28262        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28263        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28264        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28265        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28266        // Compound spans accumulate per-dimension.
28267        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28268        assert_eq!(
28269            parse_interval_text("1 day 2 hours"),
28270            Some((0, 1, 7_200_000_000))
28271        );
28272        // Negative numbers carry through per-dimension.
28273        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28274        // Bad shapes return None.
28275        assert_eq!(parse_interval_text(""), None);
28276        assert_eq!(parse_interval_text("garbage"), None);
28277        assert_eq!(parse_interval_text("1 fortnight"), None);
28278        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28279        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28280        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28281        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28282        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28283    }
28284
28285    #[test]
28286    fn interval_literal_roundtrips_via_display() {
28287        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28288        let s = parsed.to_string();
28289        // Display preserves the original text verbatim.
28290        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28291        // And re-parsing yields a structurally equal statement.
28292        let again = parse_statement(&s).unwrap();
28293        assert_eq!(parsed, again);
28294    }
28295
28296    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28297
28298    #[test]
28299    fn parser_recognises_create_publication_bare() {
28300        let s = parse("CREATE PUBLICATION pub_a");
28301        let Statement::CreatePublication(p) = s else {
28302            panic!("expected CreatePublication, got {s:?}")
28303        };
28304        assert_eq!(p.name, "pub_a");
28305        assert_eq!(p.scope, PublicationScope::AllTables);
28306    }
28307
28308    #[test]
28309    fn parser_recognises_create_publication_for_all_tables() {
28310        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28311        let Statement::CreatePublication(p) = s else {
28312            panic!("expected CreatePublication, got {s:?}")
28313        };
28314        assert_eq!(p.name, "pub_a");
28315        assert_eq!(p.scope, PublicationScope::AllTables);
28316    }
28317
28318    #[test]
28319    fn parser_recognises_drop_publication() {
28320        let s = parse("DROP PUBLICATION pub_a");
28321        let Statement::DropPublication { name, .. } = s else {
28322            panic!("expected DropPublication, got {s:?}")
28323        };
28324        assert_eq!(name, "pub_a");
28325    }
28326
28327    #[test]
28328    fn parser_recognises_for_table_list() {
28329        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28330        let Statement::CreatePublication(p) = s else {
28331            panic!("expected CreatePublication, got {s:?}")
28332        };
28333        assert_eq!(p.name, "pub_a");
28334        let PublicationScope::ForTables(ts) = p.scope else {
28335            panic!("expected ForTables scope")
28336        };
28337        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28338    }
28339
28340    #[test]
28341    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28342        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28343        // is rejected (`invalid publication object list`; the old
28344        // test pinned an unverifiable "PG 19 accepts both" claim);
28345        // TABLES pairs with IN SCHEMA.
28346        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28347            .expect_err("bare FOR TABLES must reject");
28348        assert!(
28349            alloc::format!("{err}").contains("invalid publication object list"),
28350            "got: {err}"
28351        );
28352        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28353        let Statement::CreatePublication(p) = s else {
28354            panic!("expected CreatePublication, got {s:?}")
28355        };
28356        let PublicationScope::TablesInSchema(schema) = p.scope else {
28357            panic!("expected TablesInSchema")
28358        };
28359        assert_eq!(schema, "public");
28360    }
28361
28362    #[test]
28363    fn parser_recognises_for_all_tables_except_list() {
28364        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28365        let Statement::CreatePublication(p) = s else {
28366            panic!()
28367        };
28368        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28369            panic!("expected AllTablesExcept")
28370        };
28371        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28372    }
28373
28374    #[test]
28375    fn parser_rejects_for_table_with_empty_list() {
28376        // `FOR TABLE` with nothing after is a parse error.
28377        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28378            .expect_err("must error on empty list");
28379        // No specific message asserted — the call falls through to
28380        // expect_ident_like which yields "expected identifier, got …".
28381        assert!(!err.message.is_empty());
28382    }
28383
28384    #[test]
28385    fn parser_recognises_show_publications() {
28386        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28387        // bare ident in this position, NOT a reserved keyword.
28388        let s = parse("SHOW PUBLICATIONS");
28389        assert!(matches!(s, Statement::ShowPublications));
28390    }
28391
28392    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28393
28394    #[test]
28395    fn parser_recognises_create_subscription_single_publication() {
28396        let s = parse(
28397            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28398        );
28399        let Statement::CreateSubscription(c) = s else {
28400            panic!("expected CreateSubscription, got {s:?}")
28401        };
28402        assert_eq!(c.name, "sub_a");
28403        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28404        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28405    }
28406
28407    #[test]
28408    fn parser_recognises_create_subscription_multi_publication() {
28409        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28410        let Statement::CreateSubscription(c) = s else {
28411            panic!()
28412        };
28413        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28414    }
28415
28416    #[test]
28417    fn parser_rejects_create_subscription_missing_connection() {
28418        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28419            .expect_err("must error on missing CONNECTION");
28420        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28421    }
28422
28423    #[test]
28424    fn parser_rejects_create_subscription_missing_publication() {
28425        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28426            .expect_err("must error on missing PUBLICATION");
28427        assert_eq!(err.message, "syntax error at end of input");
28428    }
28429
28430    #[test]
28431    fn parser_recognises_drop_subscription() {
28432        let s = parse("DROP SUBSCRIPTION sub_a");
28433        let Statement::DropSubscription { name, .. } = s else {
28434            panic!("expected DropSubscription, got {s:?}")
28435        };
28436        assert_eq!(name, "sub_a");
28437    }
28438
28439    #[test]
28440    fn parser_recognises_show_subscriptions() {
28441        let s = parse("SHOW SUBSCRIPTIONS");
28442        assert!(matches!(s, Statement::ShowSubscriptions));
28443    }
28444
28445    #[test]
28446    fn parser_recognises_wait_for_wal_position_no_timeout() {
28447        let s = parse("WAIT FOR WAL POSITION 12345");
28448        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28449            panic!("expected WaitForWalPosition, got {s:?}")
28450        };
28451        assert_eq!(pos, 12345);
28452        assert!(timeout_ms.is_none());
28453    }
28454
28455    #[test]
28456    fn parser_recognises_wait_for_wal_position_with_timeout() {
28457        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28458        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28459            panic!()
28460        };
28461        assert_eq!(pos, 67890);
28462        assert_eq!(timeout_ms, Some(5000));
28463    }
28464
28465    #[test]
28466    fn parser_rejects_wait_with_negative_position() {
28467        // The lexer treats `-` as a token; `expect_u64_literal`
28468        // only sees the Integer that follows, so the negative
28469        // arrives as a unary-minus expression at higher levels.
28470        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28471        // parse error one way or another.
28472        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28473        assert!(!err.message.is_empty());
28474    }
28475
28476    #[test]
28477    fn parser_recognises_bare_analyze() {
28478        let s = parse("ANALYZE");
28479        assert!(matches!(s, Statement::Analyze(None)));
28480    }
28481
28482    #[test]
28483    fn parser_recognises_analyze_with_table() {
28484        let s = parse("ANALYZE users");
28485        let Statement::Analyze(Some(name)) = s else {
28486            panic!("expected Analyze, got {s:?}")
28487        };
28488        assert_eq!(name, "users");
28489    }
28490
28491    #[test]
28492    fn parser_recognises_analyze_with_quoted_table() {
28493        let s = parse("ANALYZE \"Mixed Case\"");
28494        let Statement::Analyze(Some(name)) = s else {
28495            panic!()
28496        };
28497        assert_eq!(name, "Mixed Case");
28498    }
28499
28500    #[test]
28501    fn parser_rejects_analyze_with_garbage_token() {
28502        let err = parse_statement("ANALYZE 42").expect_err("must error");
28503        assert!(!err.message.is_empty());
28504    }
28505
28506    #[test]
28507    fn analyze_display_roundtrips() {
28508        for sql in ["ANALYZE", "ANALYZE users"] {
28509            let s = parse(sql);
28510            let printed = s.to_string();
28511            let again = parse_statement(&printed)
28512                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28513            assert_eq!(s, again);
28514        }
28515    }
28516
28517    #[test]
28518    fn wait_for_display_roundtrips() {
28519        for sql in [
28520            "WAIT FOR WAL POSITION 12345",
28521            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28522        ] {
28523            let s = parse(sql);
28524            let printed = s.to_string();
28525            let again = parse_statement(&printed)
28526                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28527            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28528        }
28529    }
28530
28531    #[test]
28532    fn subscription_ddl_display_roundtrips() {
28533        for sql in [
28534            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28535            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28536            "DROP SUBSCRIPTION sub_a",
28537            "SHOW SUBSCRIPTIONS",
28538        ] {
28539            let s = parse(sql);
28540            let printed = s.to_string();
28541            let again = parse_statement(&printed)
28542                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28543            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28544        }
28545    }
28546
28547    #[test]
28548    fn parser_drop_dispatches_user_vs_publication() {
28549        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28550        // tokenises DROP. Both targets must still parse.
28551        let s = parse("DROP USER 'alice'");
28552        let Statement::DropUser { name, .. } = s else {
28553            panic!("expected DropUser, got {s:?}")
28554        };
28555        assert_eq!(name, "alice");
28556        // And DROP PUBLICATION lands the new variant.
28557        let s = parse("DROP PUBLICATION p1");
28558        assert!(matches!(s, Statement::DropPublication { .. }));
28559    }
28560
28561    #[test]
28562    fn publication_ddl_display_roundtrips() {
28563        // Every CREATE PUBLICATION variant must Display → parse →
28564        // same AST. v6.1.3 covers all three scope shapes.
28565        for sql in [
28566            "CREATE PUBLICATION pub_a",
28567            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28568            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28569            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28570            "DROP PUBLICATION pub_a",
28571            "SHOW PUBLICATIONS",
28572        ] {
28573            let s = parse(sql);
28574            let printed = s.to_string();
28575            let again = parse_statement(&printed)
28576                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28577            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28578        }
28579    }
28580
28581    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28582
28583    #[test]
28584    fn create_function_returns_trigger_plpgsql_minimal() {
28585        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28586        let s = parse(sql);
28587        let Statement::CreateFunction(f) = s else {
28588            panic!("expected CreateFunction");
28589        };
28590        assert_eq!(f.name, "noop");
28591        assert!(!f.or_replace);
28592        assert!(f.args.is_empty());
28593        assert!(matches!(f.returns, FunctionReturn::Trigger));
28594        assert_eq!(f.language, "plpgsql");
28595        let FunctionBody::PlPgSql(block) = f.body else {
28596            panic!("expected PlPgSql body");
28597        };
28598        assert_eq!(block.statements.len(), 1);
28599        assert!(matches!(
28600            block.statements[0],
28601            PlPgSqlStmt::Return(ReturnTarget::New)
28602        ));
28603    }
28604
28605    #[test]
28606    fn create_function_or_replace_with_assignment() {
28607        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28608        // RETURN NEW.
28609        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28610BEGIN
28611  NEW.search_vector := to_tsvector('english', NEW.subject);
28612  RETURN NEW;
28613END;
28614$$";
28615        let s = parse(sql);
28616        let Statement::CreateFunction(f) = s else {
28617            panic!("expected CreateFunction");
28618        };
28619        assert!(f.or_replace);
28620        let FunctionBody::PlPgSql(block) = &f.body else {
28621            panic!("expected PlPgSql body");
28622        };
28623        assert_eq!(block.statements.len(), 2);
28624        // First statement: NEW.search_vector := to_tsvector(...)
28625        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28626            panic!("expected Assign as first stmt");
28627        };
28628        match target {
28629            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28630            other => panic!("expected NEW.col, got {other:?}"),
28631        }
28632        // Second statement: RETURN NEW
28633        assert!(matches!(
28634            block.statements[1],
28635            PlPgSqlStmt::Return(ReturnTarget::New)
28636        ));
28637    }
28638
28639    #[test]
28640    fn create_trigger_after_insert_or_update() {
28641        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28642        let s = parse(sql);
28643        let Statement::CreateTrigger(t) = s else {
28644            panic!("expected CreateTrigger");
28645        };
28646        assert_eq!(t.name, "tg");
28647        assert_eq!(t.table, "messages");
28648        assert_eq!(t.timing, TriggerTiming::After);
28649        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28650        assert_eq!(t.for_each, TriggerForEach::Row);
28651        assert_eq!(t.function, "update_sv");
28652    }
28653
28654    #[test]
28655    fn create_trigger_before_delete_execute_procedure_alias() {
28656        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28657        let sql =
28658            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28659        let s = parse(sql);
28660        let Statement::CreateTrigger(t) = s else {
28661            panic!("expected CreateTrigger");
28662        };
28663        assert_eq!(t.timing, TriggerTiming::Before);
28664        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28665    }
28666
28667    #[test]
28668    fn drop_trigger_if_exists_round_trips() {
28669        // No parser support for DROP TRIGGER yet — added in v7.12.5
28670        // alongside the broader DROP …{IF EXISTS} cleanup. The
28671        // AST + Display impls are in place so we round-trip via
28672        // construction:
28673        let s = Statement::DropTrigger {
28674            name: "tg".into(),
28675            table: "messages".into(),
28676            if_exists: true,
28677        };
28678        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28679    }
28680
28681    #[test]
28682    fn trigger_ddl_display_roundtrips_through_parser() {
28683        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28684        // Display → parse → same AST (modulo PL/pgSQL body
28685        // formatting which is parser-canonicalised).
28686        for sql in [
28687            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28688            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28689        ] {
28690            let s = parse(sql);
28691            let printed = s.to_string();
28692            let again = parse_statement(&printed)
28693                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28694            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28695        }
28696    }
28697}