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                param_types.push(ty);
1686                match self.peek() {
1687                    Token::Comma => {
1688                        self.advance();
1689                    }
1690                    Token::RParen => {
1691                        self.advance();
1692                        break;
1693                    }
1694                    other => {
1695                        return Err(self.err(alloc::format!(
1696                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1697                        )));
1698                    }
1699                }
1700            }
1701        }
1702        if !matches!(self.peek(), Token::As) {
1703            return Err(self.err(alloc::format!(
1704                "expected AS in PREPARE, got {:?}",
1705                self.peek()
1706            )));
1707        }
1708        self.advance();
1709        let body = self.parse_one_statement()?;
1710        // The Parser holds tokens, not the source text, so the
1711        // statement PG reports in `pg_prepared_statements.statement`
1712        // is rebuilt from the AST rather than sliced from the input.
1713        let _ = start;
1714        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1715        if !param_types.is_empty() {
1716            source.push_str(" (");
1717            source.push_str(&param_types.join(", "));
1718            source.push(')');
1719        }
1720        source.push_str(" AS ");
1721        source.push_str(&alloc::format!("{body}"));
1722        Ok(Statement::Prepare {
1723            name,
1724            param_types,
1725            body: alloc::boxed::Box::new(body),
1726            source,
1727        })
1728    }
1729
1730    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1731    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1732        self.advance(); // EXECUTE
1733        let name = self.expect_ident_like()?;
1734        let mut args = Vec::new();
1735        if matches!(self.peek(), Token::LParen) {
1736            self.advance();
1737            if matches!(self.peek(), Token::RParen) {
1738                self.advance();
1739            } else {
1740                loop {
1741                    args.push(self.parse_expr(0)?);
1742                    match self.advance() {
1743                        Token::Comma => {}
1744                        Token::RParen => break,
1745                        other => {
1746                            return Err(self.err(alloc::format!(
1747                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1748                            )));
1749                        }
1750                    }
1751                }
1752            }
1753        }
1754        Ok(Statement::Execute { name, args })
1755    }
1756
1757    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1758    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1759    /// procedure catalog yet, so this reports PG's not-found error
1760    /// (with its HINT) rather than pretending the call ran.
1761    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1762    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1763    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1764        self.advance(); // DISCARD
1765        let target = match self.advance() {
1766            Token::All => DiscardTarget::All,
1767            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1768                "all" => DiscardTarget::All,
1769                "plans" => DiscardTarget::Plans,
1770                "sequences" => DiscardTarget::Sequences,
1771                "temp" | "temporary" => DiscardTarget::Temp,
1772                other => {
1773                    return Err(self.err(format!(
1774                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1775                    )));
1776                }
1777            },
1778            other => {
1779                return Err(self.err(format!(
1780                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1781                )));
1782            }
1783        };
1784        Ok(Statement::Discard(target))
1785    }
1786
1787    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1788    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1789    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1790    /// aggressively the server interrupts, which SPG does not distinguish.
1791    /// Bare `KILL <id>` means CONNECTION.
1792    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1793        self.advance(); // KILL
1794        let mut query_only = false;
1795        loop {
1796            // CONNECTION is a reserved keyword token (it also opens
1797            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1798            // `Token::Connection` rather than a bare ident.
1799            if matches!(self.peek(), Token::Connection) {
1800                self.advance();
1801                break;
1802            }
1803            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1804                break;
1805            };
1806            match w.to_ascii_lowercase().as_str() {
1807                "hard" | "soft" => {
1808                    self.advance();
1809                }
1810                "query" => {
1811                    self.advance();
1812                    query_only = true;
1813                    break;
1814                }
1815                _ => break,
1816            }
1817        }
1818        let id = self.parse_expr(0)?;
1819        Ok(Statement::Kill {
1820            query_only,
1821            id: Box::new(id),
1822        })
1823    }
1824
1825    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1826        self.advance(); // CALL
1827        let name = self.expect_ident_like()?;
1828        self.consume_until_statement_boundary();
1829        Ok(Statement::Call(name))
1830    }
1831
1832    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1833        self.advance(); // DEALLOCATE
1834        // PG accepts an optional noise `PREPARE` keyword here.
1835        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1836            self.advance();
1837        }
1838        if matches!(self.peek(), Token::All) {
1839            self.advance();
1840            return Ok(Statement::Deallocate(None));
1841        }
1842        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1843            self.advance();
1844            return Ok(Statement::Deallocate(None));
1845        }
1846        let name = self.expect_ident_like()?;
1847        Ok(Statement::Deallocate(Some(name)))
1848    }
1849
1850    fn consume_until_statement_boundary(&mut self) {
1851        loop {
1852            match self.peek() {
1853                Token::Semicolon | Token::Eof => return,
1854                _ => self.advance(),
1855            };
1856        }
1857    }
1858
1859    /// v7.22 (round-13 T2) — consume to the statement boundary like
1860    /// `consume_until_statement_boundary`, but pick out the sequence
1861    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1862    /// columns) or the first string literal (`nextval('<seq>')`).
1863    /// Schema qualifiers and `::regclass` casts are stripped.
1864    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1865        let mut seq: Option<String> = None;
1866        let mut after_sequence_kw = false;
1867        let mut after_name_kw = false;
1868        loop {
1869            match self.peek().clone() {
1870                Token::Semicolon | Token::Eof => break,
1871                Token::Ident(s) | Token::QuotedIdent(s) => {
1872                    if after_name_kw && seq.is_none() {
1873                        self.advance();
1874                        let mut name = s;
1875                        // `SEQUENCE NAME public.groups_id_seq` — keep
1876                        // the bare name, drop qualifiers.
1877                        while matches!(self.peek(), Token::Dot) {
1878                            self.advance();
1879                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1880                                name = n;
1881                            }
1882                        }
1883                        seq = Some(name);
1884                        after_name_kw = false;
1885                        continue;
1886                    }
1887                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1888                        after_name_kw = true;
1889                        after_sequence_kw = false;
1890                    } else {
1891                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1892                    }
1893                    self.advance();
1894                }
1895                Token::String(s) => {
1896                    if seq.is_none() {
1897                        // `nextval('public.groups_id_seq'::regclass)`
1898                        let bare = s
1899                            .rsplit_once('.')
1900                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1901                        seq = Some(bare);
1902                    }
1903                    self.advance();
1904                }
1905                _ => {
1906                    after_sequence_kw = false;
1907                    after_name_kw = false;
1908                    self.advance();
1909                }
1910            }
1911        }
1912        seq
1913    }
1914
1915    /// v7.39 (round 621) — is the next token the keyword `BY`?
1916    ///
1917    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1918    /// column, table and alias name — and SPG lexed it into a dedicated
1919    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1920    /// two-letter keywords the lexer knew, this was the only one PG leaves
1921    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1922    ///
1923    /// The token is gone; the three clauses that own the word — GROUP BY,
1924    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1925    /// ask this instead. Adding it to the unreserved-identifier table was not
1926    /// enough on its own: identifier positions that match the token shape
1927    /// directly (an index's column list, a table alias) never consult that
1928    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1929    /// Not lexing it as a keyword closes the whole class rather than the two
1930    /// positions that happened to be noticed.
1931    fn peek_is_by(&self) -> bool {
1932        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1933    }
1934
1935    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1936    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1937    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1938    fn consume_drop_behaviour(&mut self) {
1939        if matches!(
1940            self.peek(),
1941            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1942        ) {
1943            self.advance();
1944        }
1945    }
1946
1947    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1948        let first = match self.advance() {
1949            Token::Ident(s) | Token::QuotedIdent(s) => s,
1950            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1951            // per PG's `pg_get_keywords()` classification. SPG tokenizes
1952            // these as named variants for parsing leverage in the
1953            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1954            // `BEGIN`, etc.), but they MUST still be usable as table /
1955            // column / alias names in DDL+DML. Sentori migrations like
1956            // 0001_init.sql ship `release TEXT NOT NULL` in the events
1957            // table — the `events.release` column carries the release
1958            // identifier string. Pre-T4 this triggered "expected
1959            // identifier, got Release" and blocked every drop-in user
1960            // whose schema had a column / alias with one of these names.
1961            other if unreserved_keyword_text(&other).is_some() => {
1962                unreserved_keyword_text(&other).unwrap()
1963            }
1964            other => {
1965                return Err(ParseError {
1966                    message: format!("expected identifier, got {other:?}"),
1967                    token_pos: self.consumed_pos(),
1968                });
1969            }
1970        };
1971        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1972        // qualify every name with `public.` (and pg_catalog.* for
1973        // functions); SPG is single-schema so we discard the
1974        // prefix and return only the trailing ident. Same shape
1975        // also handles MySQL `db.tbl` cross-database refs (SPG
1976        // ignores the db part).
1977        if matches!(self.peek(), Token::Dot) {
1978            self.advance();
1979            match self.advance() {
1980                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1981                other if unreserved_keyword_text(&other).is_some() => {
1982                    return Ok(unreserved_keyword_text(&other).unwrap());
1983                }
1984                other => {
1985                    return Err(ParseError {
1986                        message: format!("expected identifier after '{first}.', got {other:?}"),
1987                        token_pos: self.consumed_pos(),
1988                    });
1989                }
1990            }
1991        }
1992        Ok(first)
1993    }
1994
1995    #[allow(clippy::too_many_lines)]
1996    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
1997        // v7.14.0 — empty / comment-only / semicolon-only input
1998        // (after the lexer strips line + block + MySQL
1999        // conditional comments) lands as Statement::Empty.
2000        // pg_dump and mysqldump emit several wrappers that
2001        // collapse to nothing after stripping (`/*!40101 SET …
2002        // */;`, blank lines between statements); the engine
2003        // returns CommandOk no-op so the dump loads cleanly.
2004        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2005            return Ok(Statement::Empty);
2006        }
2007        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2008        // catalog / metadata DDL that has no behavioural effect
2009        // on SPG's single-schema, single-database, single-user
2010        // model. Consume the whole statement up to the next
2011        // semicolon / EOF and return Empty. This is broader than
2012        // the per-keyword DROP / SET / COMMENT arms but lets the
2013        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2014        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2015        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2016        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2017            let lc = s.to_ascii_lowercase();
2018            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2019            if lc == "comment" {
2020                return self.parse_comment_on();
2021            }
2022            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2023            if lc == "grant" || lc == "revoke" {
2024                return self.parse_grant_or_revoke(lc == "grant");
2025            }
2026            // v7.39 (round 277) — the SQL-level prepared-statement
2027            // surface is REAL now. It used to be accepted and dropped
2028            // on the theory that "real execution still happens via the
2029            // extended-query flow" — true only for a driver that uses
2030            // that flow; a plain SQL PREPARE / EXECUTE returned no
2031            // rows at all.
2032            if lc == "prepare" {
2033                return self.parse_prepare();
2034            }
2035            if lc == "execute" {
2036                return self.parse_execute();
2037            }
2038            if lc == "deallocate" {
2039                return self.parse_deallocate();
2040            }
2041            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2042            // accepted and dropped, so an application's stored-procedure
2043            // invocation reported success and did nothing. SPG has no
2044            // procedure catalog, so every CALL names a procedure that
2045            // does not exist — which is exactly what PG says.
2046            if lc == "call" {
2047                return self.parse_call();
2048            }
2049            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2050            // names one connection and acts on it.
2051            if lc == "kill" {
2052                return self.parse_kill();
2053            }
2054            if lc == "discard" {
2055                return self.parse_discard();
2056            }
2057            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2058            // Still performs nothing; the roles are carried out so a name
2059            // that does not exist is refused, as PG18 refuses it.
2060            if lc == "reassign" {
2061                self.advance();
2062                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2063                    self.advance();
2064                }
2065                if self.peek_is_by() {
2066                    self.advance();
2067                }
2068                // Only the roles BEFORE the TO are the ones that must
2069                // exist — `TO` names the new owner, which PG checks as
2070                // well, so both lists are collected.
2071                let mut names = self.take_comma_separated_names();
2072                if matches!(self.peek(), Token::To) {
2073                    self.advance();
2074                    names.extend(self.take_comma_separated_names());
2075                }
2076                self.consume_until_statement_boundary();
2077                return Ok(Statement::ValidateOnly {
2078                    kind: crate::ast::ValidateOnlyKind::RoleName,
2079                    names,
2080                });
2081            }
2082            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2083            // unconditionally with `no security label providers have been
2084            // loaded`, whatever object it names, because none is loaded.
2085            // SPG has none either; accepting it told the caller a label had
2086            // been applied when nothing anywhere records one.
2087            if lc == "security" {
2088                self.consume_until_statement_boundary();
2089                return Ok(Statement::ValidateOnly {
2090                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2091                    names: Vec::new(),
2092                });
2093            }
2094            if is_dump_noise_statement(&lc) {
2095                self.consume_until_statement_boundary();
2096                return Ok(Statement::Empty);
2097            }
2098        }
2099        match self.peek() {
2100            Token::Select => self.parse_select_stmt(),
2101            // v7.37.17 (17.6 siblings) — a statement opening with a
2102            // parenthesized query group: `(SELECT … UNION …)
2103            // INTERSECT …`. parse_bare_select's group arm consumes
2104            // the parens; the select parser handles the outer chain
2105            // and tail.
2106            Token::LParen
2107                if matches!(
2108                    self.tokens.get(self.pos + 1),
2109                    Some(Token::Select | Token::LParen | Token::Values)
2110                ) =>
2111            {
2112                self.parse_select_stmt()
2113            }
2114            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2115            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2116            // Lowers to the same UNION ALL chain the FROM-position
2117            // form uses, then reuses the shared SELECT tail.
2118            Token::Values => {
2119                self.advance(); // VALUES
2120                let mut head = self.parse_values_rows_body()?;
2121                self.parse_select_tail_into(&mut head)?;
2122                Ok(Statement::Select(head))
2123            }
2124            // SQL-standard `TABLE name` shorthand for
2125            // `SELECT * FROM name` — pg_dump never emits it, but
2126            // psql users and PG docs use it constantly. Set-op
2127            // chains and the ORDER BY/LIMIT tail compose like any
2128            // SELECT head.
2129            Token::Table
2130                if matches!(
2131                    self.tokens.get(self.pos + 1),
2132                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2133                ) =>
2134            {
2135                let mut head = self.parse_table_shorthand()?;
2136                self.parse_setop_chain_into(&mut head)?;
2137                self.parse_select_tail_into(&mut head)?;
2138                Ok(Statement::Select(head))
2139            }
2140            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2141            // body is a dollar-quoted plpgsql block (lexer already
2142            // collapsed `$$…$$` into a single Token::String).
2143            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2144            // real PlPgSqlBlock so the engine can EXECUTE it at
2145            // top level instead of silently swallowing. Pre-
2146            // v7.16.2 the parser threw the body away and the
2147            // engine returned CommandOk for the entire DO; that
2148            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2149            // $$` into a SEV-1 silent no-op (the IF + the rename
2150            // were both invisible — mailrs's migrate-042 didn't
2151            // actually run). Now the body parses + executes;
2152            // EmbeddedSql inside the block runs immediately
2153            // against the engine (not deferred — we're at top
2154            // level, not inside a trigger row-write loop).
2155            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2156                self.advance();
2157                let body_text = match self.advance() {
2158                    Token::String(s) => s,
2159                    other => {
2160                        return Err(self.err(alloc::format!(
2161                            "expected dollar-quoted body after DO, got {other:?}"
2162                        )));
2163                    }
2164                };
2165                // Optional `LANGUAGE <name>` trailer (idents only).
2166                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2167                    self.advance();
2168                    let _ = self.expect_ident_like()?;
2169                }
2170                // Parse the body — same shape CREATE FUNCTION
2171                // uses for trigger function bodies. If the body
2172                // doesn't parse cleanly we surface the error
2173                // (better than silent no-op).
2174                let block = parse_plpgsql_body(&body_text)?;
2175                Ok(Statement::DoBlock(block))
2176            }
2177            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2178            // WITH isn't a reserved token in our lexer — comes through
2179            // as `Token::Ident("with")` (case-insensitive).
2180            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2181                self.advance();
2182                self.parse_with_cte_then_select()
2183            }
2184            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2185            // an identifier — not a reserved keyword.
2186            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2187                self.advance();
2188                let mut analyze = false;
2189                let mut suggest = false;
2190                let mut costs_off = false;
2191                let mut buffers = false;
2192                let mut timing_off = false;
2193                let mut settings = false;
2194                let mut wal = false;
2195                let mut summary_off = false;
2196                let mut format = crate::ast::ExplainFormat::Text;
2197                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2198                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2199                // options are comma-separated. Booleans default to ON
2200                // when the value token is omitted (matches PG).
2201                if matches!(self.peek(), Token::LParen) {
2202                    self.advance();
2203                    loop {
2204                        let opt = match self.peek().clone() {
2205                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2206                            other => {
2207                                return Err(self.err(format!(
2208                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2209                                )));
2210                            }
2211                        };
2212                        self.advance();
2213                        if opt.eq_ignore_ascii_case("suggest") {
2214                            suggest = true;
2215                            // SUGGEST takes no explicit value today.
2216                        } else if opt.eq_ignore_ascii_case("costs") {
2217                            // PG syntax: `COSTS [ON | OFF]`. Default
2218                            // when value omitted is ON, so plain
2219                            // `COSTS` is a no-op. `COSTS OFF` flips.
2220                            // `ON` lexes to `Token::On` (reserved
2221                            // keyword in JOIN ... ON contexts); accept
2222                            // it alongside the bare Ident form so the
2223                            // grammar matches PG verbatim.
2224                            let value = match self.peek().clone() {
2225                                Token::On => {
2226                                    self.advance();
2227                                    true
2228                                }
2229                                Token::Ident(v) | Token::QuotedIdent(v)
2230                                    if v.eq_ignore_ascii_case("off") =>
2231                                {
2232                                    self.advance();
2233                                    false
2234                                }
2235                                Token::Ident(v) | Token::QuotedIdent(v)
2236                                    if v.eq_ignore_ascii_case("true") =>
2237                                {
2238                                    self.advance();
2239                                    true
2240                                }
2241                                _ => true,
2242                            };
2243                            costs_off = !value;
2244                        } else if opt.eq_ignore_ascii_case("analyze")
2245                            || opt.eq_ignore_ascii_case("analyse")
2246                        {
2247                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2248                            // Same default-ON rule as ANALYZE keyword form.
2249                            let value = match self.peek().clone() {
2250                                Token::On => {
2251                                    self.advance();
2252                                    true
2253                                }
2254                                Token::Ident(v) | Token::QuotedIdent(v)
2255                                    if v.eq_ignore_ascii_case("off") =>
2256                                {
2257                                    self.advance();
2258                                    false
2259                                }
2260                                Token::Ident(v) | Token::QuotedIdent(v)
2261                                    if v.eq_ignore_ascii_case("true") =>
2262                                {
2263                                    self.advance();
2264                                    true
2265                                }
2266                                _ => true,
2267                            };
2268                            analyze = value;
2269                        } else if opt.eq_ignore_ascii_case("buffers") {
2270                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2271                            let value = match self.peek().clone() {
2272                                Token::On => {
2273                                    self.advance();
2274                                    true
2275                                }
2276                                Token::Ident(v) | Token::QuotedIdent(v)
2277                                    if v.eq_ignore_ascii_case("off") =>
2278                                {
2279                                    self.advance();
2280                                    false
2281                                }
2282                                Token::Ident(v) | Token::QuotedIdent(v)
2283                                    if v.eq_ignore_ascii_case("true") =>
2284                                {
2285                                    self.advance();
2286                                    true
2287                                }
2288                                _ => true,
2289                            };
2290                            buffers = value;
2291                        } else if opt.eq_ignore_ascii_case("timing") {
2292                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2293                            // the measured wall-clock annotation.
2294                            let value = match self.peek().clone() {
2295                                Token::On => {
2296                                    self.advance();
2297                                    true
2298                                }
2299                                Token::Ident(v) | Token::QuotedIdent(v)
2300                                    if v.eq_ignore_ascii_case("off") =>
2301                                {
2302                                    self.advance();
2303                                    false
2304                                }
2305                                Token::Ident(v) | Token::QuotedIdent(v)
2306                                    if v.eq_ignore_ascii_case("true") =>
2307                                {
2308                                    self.advance();
2309                                    true
2310                                }
2311                                _ => true,
2312                            };
2313                            timing_off = !value;
2314                        } else if opt.eq_ignore_ascii_case("settings") {
2315                            settings = true;
2316                        } else if opt.eq_ignore_ascii_case("wal") {
2317                            wal = true;
2318                        } else if opt.eq_ignore_ascii_case("summary") {
2319                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2320                            // gates the trailing Planning/Execution Time
2321                            // lines now (was accept-and-no-op).
2322                            let value = match self.peek().clone() {
2323                                Token::On => {
2324                                    self.advance();
2325                                    true
2326                                }
2327                                Token::Ident(v) | Token::QuotedIdent(v)
2328                                    if v.eq_ignore_ascii_case("off") =>
2329                                {
2330                                    self.advance();
2331                                    false
2332                                }
2333                                Token::Ident(v) | Token::QuotedIdent(v)
2334                                    if v.eq_ignore_ascii_case("true") =>
2335                                {
2336                                    self.advance();
2337                                    true
2338                                }
2339                                _ => true,
2340                            };
2341                            summary_off = !value;
2342                        } else if opt.eq_ignore_ascii_case("verbose")
2343                            || opt.eq_ignore_ascii_case("format")
2344                        {
2345                            // v7.37.22 — accept-but-no-op the remaining
2346                            // PG options so EXPLAIN-using clients
2347                            // (pgAdmin / DataGrip) don't see syntax
2348                            // errors. FORMAT takes a value (text /
2349                            // json / yaml / xml); skip the next token
2350                            // if it's an ident.
2351                            if opt.eq_ignore_ascii_case("format") {
2352                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2353                                {
2354                                    self.advance();
2355                                    format = match v.to_ascii_lowercase().as_str() {
2356                                        "text" => crate::ast::ExplainFormat::Text,
2357                                        "json" => crate::ast::ExplainFormat::Json,
2358                                        "xml" => crate::ast::ExplainFormat::Xml,
2359                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2360                                        other => {
2361                                            return Err(self.err(format!(
2362                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2363                                                 supports text, json, xml, yaml"
2364                                            )));
2365                                        }
2366                                    };
2367                                }
2368                            } else {
2369                                // VERBOSE / SUMMARY take optional ON/OFF;
2370                                // consume if present.
2371                                if matches!(self.peek(), Token::On) {
2372                                    self.advance();
2373                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2374                                    self.peek().clone()
2375                                    && (v.eq_ignore_ascii_case("off")
2376                                        || v.eq_ignore_ascii_case("true"))
2377                                {
2378                                    self.advance();
2379                                    let _ = v;
2380                                }
2381                            }
2382                        } else {
2383                            return Err(self.err(format!(
2384                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2385                            )));
2386                        }
2387                        if matches!(self.peek(), Token::Comma) {
2388                            self.advance();
2389                            continue;
2390                        }
2391                        break;
2392                    }
2393                    if !matches!(self.peek(), Token::RParen) {
2394                        return Err(self.err(format!(
2395                            "expected ')' after EXPLAIN options, got {:?}",
2396                            self.peek()
2397                        )));
2398                    }
2399                    self.advance();
2400                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2401                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2402                {
2403                    self.advance();
2404                    analyze = true;
2405                }
2406                // v7.39 (round 224) — the body may open with WITH (CTEs);
2407                // route through the same CTE-then-SELECT path the top-level
2408                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2409                // too (PG explains INSERT / UPDATE / DELETE).
2410                let inner = match self.peek().clone() {
2411                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2412                        self.advance();
2413                        self.parse_with_cte_then_select()?
2414                    }
2415                    Token::Insert => self.parse_insert_stmt(false)?,
2416                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2417                        self.advance();
2418                        self.parse_update_after_keyword()?
2419                    }
2420                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2421                        self.advance();
2422                        self.parse_delete_after_keyword()?
2423                    }
2424                    _ => self.parse_select_stmt()?,
2425                };
2426                if !matches!(
2427                    inner,
2428                    Statement::Select(_)
2429                        | Statement::Insert(_)
2430                        | Statement::Update(_)
2431                        | Statement::Delete(_)
2432                ) {
2433                    return Err(self.err(format!(
2434                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2435                    )));
2436                }
2437                Ok(Statement::Explain(crate::ast::ExplainStatement {
2438                    analyze,
2439                    inner: Box::new(inner),
2440                    suggest,
2441                    costs_off,
2442                    buffers,
2443                    timing_off,
2444                    settings,
2445                    wal,
2446                    summary_off,
2447                    format,
2448                }))
2449            }
2450            Token::Create => self.parse_create_stmt(),
2451            Token::Insert => self.parse_insert_stmt(false),
2452            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2453            // spelling; route to the same handler. DESC is the
2454            // reserved ORDER BY token, so it gets its own arm.
2455            Token::Ident(s)
2456                if s.eq_ignore_ascii_case("describe")
2457                    && matches!(
2458                        self.tokens.get(self.pos + 1),
2459                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2460                    ) =>
2461            {
2462                self.advance();
2463                let table = self.expect_ident_like()?;
2464                Ok(Statement::ShowColumns(table))
2465            }
2466            Token::Desc
2467                if matches!(
2468                    self.tokens.get(self.pos + 1),
2469                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2470                ) =>
2471            {
2472                self.advance();
2473                let table = self.expect_ident_like()?;
2474                Ok(Statement::ShowColumns(table))
2475            }
2476            // `COPY table [(cols)] TO STDOUT` — the export half of
2477            // pg_dump's COPY pair (the FROM stdin half rides the
2478            // embed import path). Options need a format design and
2479            // error honestly.
2480            Token::Ident(s)
2481                if s.eq_ignore_ascii_case("copy")
2482                    && matches!(
2483                        self.tokens.get(self.pos + 1),
2484                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2485                    ) =>
2486            {
2487                self.advance(); // COPY
2488                let table = self.expect_ident_like()?;
2489                let columns = if matches!(self.peek(), Token::LParen) {
2490                    self.advance();
2491                    let mut cols = alloc::vec![self.expect_ident_like()?];
2492                    while matches!(self.peek(), Token::Comma) {
2493                        self.advance();
2494                        cols.push(self.expect_ident_like()?);
2495                    }
2496                    if !matches!(self.peek(), Token::RParen) {
2497                        return Err(self.err(format!(
2498                            "expected ')' after COPY column list, got {:?}",
2499                            self.peek()
2500                        )));
2501                    }
2502                    self.advance();
2503                    Some(cols)
2504                } else {
2505                    None
2506                };
2507                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2508                // endpoint. (FROM STDIN still rides the wire/import path —
2509                // its data arrives out of band.)
2510                if matches!(self.peek(), Token::From)
2511                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2512                {
2513                    self.advance(); // FROM
2514                    let Token::String(path) = self.advance() else {
2515                        unreachable!()
2516                    };
2517                    let options = self.parse_copy_to_options()?;
2518                    return Ok(Statement::CopyFromFile {
2519                        table,
2520                        columns,
2521                        path,
2522                        options,
2523                    });
2524                }
2525                if !matches!(self.peek(), Token::To) {
2526                    return Err(self.err(format!(
2527                        "COPY: only TO STDOUT is supported here (FROM stdin \
2528                         rides the import path); got {:?}",
2529                        self.peek()
2530                    )));
2531                }
2532                self.advance();
2533                if matches!(self.peek(), Token::String(_)) {
2534                    let Token::String(path) = self.advance() else { unreachable!() };
2535                    let options = self.parse_copy_to_options()?;
2536                    return Ok(Statement::CopyToFile {
2537                        table,
2538                        columns,
2539                        query: None,
2540                        path,
2541                        options,
2542                    });
2543                }
2544                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2545                    return Err(self.err(format!(
2546                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2547                        self.peek()
2548                    )));
2549                }
2550                self.advance();
2551                let options = self.parse_copy_to_options()?;
2552                Ok(Statement::CopyTo {
2553                    table,
2554                    columns,
2555                    query: None,
2556                    options,
2557                })
2558            }
2559            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2560            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2561            // result set is streamed in COPY format (PG's query form).
2562            Token::Ident(s)
2563                if s.eq_ignore_ascii_case("copy")
2564                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2565            {
2566                self.advance(); // COPY
2567                self.advance(); // (
2568                let query = self.parse_select_stmt()?;
2569                if !matches!(self.peek(), Token::RParen) {
2570                    return Err(self.err(format!(
2571                        "expected ')' after COPY query, got {:?}",
2572                        self.peek()
2573                    )));
2574                }
2575                self.advance(); // )
2576                if !matches!(self.peek(), Token::To) {
2577                    return Err(self.err(format!(
2578                        "COPY (query): only TO STDOUT is supported, got {:?}",
2579                        self.peek()
2580                    )));
2581                }
2582                self.advance();
2583                if matches!(self.peek(), Token::String(_)) {
2584                    let Token::String(path) = self.advance() else { unreachable!() };
2585                    let options = self.parse_copy_to_options()?;
2586                    return Ok(Statement::CopyToFile {
2587                        table: String::new(),
2588                        columns: None,
2589                        query: Some(alloc::boxed::Box::new(query)),
2590                        path,
2591                        options,
2592                    });
2593                }
2594                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2595                    return Err(self.err(format!(
2596                        "COPY (query): TO supports STDOUT only, got {:?}",
2597                        self.peek()
2598                    )));
2599                }
2600                self.advance();
2601                let options = self.parse_copy_to_options()?;
2602                Ok(Statement::CopyTo {
2603                    table: String::new(),
2604                    columns: None,
2605                    query: Some(alloc::boxed::Box::new(query)),
2606                    options,
2607                })
2608            }
2609            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2610            // Shares the INSERT body; the replace flag lowers it
2611            // onto ON CONFLICT DO UPDATE with an empty assignment
2612            // list (engine: replace the whole row).
2613            Token::Ident(s)
2614                if s.eq_ignore_ascii_case("replace")
2615                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2616            {
2617                self.parse_insert_stmt(true)
2618            }
2619            Token::Begin => {
2620                self.advance();
2621                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2622                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2623                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2624                // is consumed first, then the trailing modes — including the
2625                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2626                // WORK/TRANSACTION). The explicit level, when present, rides the
2627                // statement so `exec_begin` applies it for this transaction.
2628                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2629                {
2630                    self.advance();
2631                }
2632                let iso = self.parse_isolation_level_clauses()?;
2633                Ok(Statement::Begin(iso))
2634            }
2635            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2636            // for BEGIN. START is contextual in PG too; pattern-match
2637            // on the ident here. Iso clauses are parse-and-ignored,
2638            // same as BEGIN above.
2639            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2640                self.advance();
2641                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2642                {
2643                    return Err(self.err(alloc::format!(
2644                        "expected TRANSACTION after START, got {:?}",
2645                        self.peek()
2646                    )));
2647                }
2648                self.advance();
2649                let iso = self.parse_isolation_level_clauses()?;
2650                Ok(Statement::Begin(iso))
2651            }
2652            Token::Commit => {
2653                self.advance();
2654                Ok(Statement::Commit)
2655            }
2656            Token::Rollback => {
2657                self.advance();
2658                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2659                // savepoint without ending the transaction. Bare
2660                // `ROLLBACK` drops the whole TX.
2661                if matches!(self.peek(), Token::To) {
2662                    self.advance();
2663                    if matches!(self.peek(), Token::Savepoint) {
2664                        self.advance();
2665                    }
2666                    let name = self.expect_ident_like()?;
2667                    Ok(Statement::RollbackToSavepoint(name))
2668                } else {
2669                    Ok(Statement::Rollback)
2670                }
2671            }
2672            Token::Savepoint => {
2673                self.advance();
2674                let name = self.expect_ident_like()?;
2675                Ok(Statement::Savepoint(name))
2676            }
2677            Token::Release => {
2678                self.advance();
2679                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2680                // is optional in standard SQL.
2681                if matches!(self.peek(), Token::Savepoint) {
2682                    self.advance();
2683                }
2684                let name = self.expect_ident_like()?;
2685                Ok(Statement::ReleaseSavepoint(name))
2686            }
2687            Token::Show => {
2688                self.advance();
2689                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2690                // v6.1.2 promoted TABLES to a reserved keyword (for
2691                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2692                // arrives as `Token::Tables` rather than a bare ident.
2693                // USERS / COLUMNS remain bare idents.
2694                let target = match self.advance() {
2695                    Token::Tables => "tables".to_string(),
2696                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2697                    // keyword token; recognise it as the SHOW CREATE
2698                    // dispatch keyword too.
2699                    Token::Create => "create".to_string(),
2700                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2701                    // keyword too; let SHOW INDEX FROM parse.
2702                    Token::Index => "index".to_string(),
2703                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2704                    // reserved (used in aggregate function calls);
2705                    // recognise it here so the parser dispatches
2706                    // to ShowParameter("all") — the engine returns
2707                    // the curated parameter inventory.
2708                    Token::All => "all".to_string(),
2709                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2710                    other => {
2711                        return Err(self.err(format!(
2712                            "expected SHOW target, got {other:?}"
2713                        )));
2714                    }
2715                };
2716                match target.as_str() {
2717                    "tables" => Ok(Statement::ShowTables),
2718                    "users" => Ok(Statement::ShowUsers),
2719                    // v7.38 轴 4 — `SHOW transaction_isolation`
2720                    // returns the currently-selected isolation level.
2721                    "transaction_isolation" => Ok(Statement::ShowParameter(
2722                        "transaction_isolation".to_string(),
2723                    )),
2724                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2725                    // TABLE <t>` returns a 2-column row: (Table,
2726                    // Create Table). mysqldump emits this for every
2727                    // table at scrape time; without it the dump
2728                    // round-trip stalls.
2729                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2730                    // FROM <t>` (also spelled `SHOW INDEX` and
2731                    // `SHOW KEYS`). admin / mysqldump probes use
2732                    // it to list per-table indexes.
2733                    "indexes" | "index" | "keys" => {
2734                        if !matches!(self.peek(), Token::From) {
2735                            return Err(self.err(format!(
2736                                "expected FROM after SHOW INDEXES, got {:?}",
2737                                self.peek()
2738                            )));
2739                        }
2740                        self.advance();
2741                        let table = self.expect_ident_like()?;
2742                        Ok(Statement::ShowIndexes(table))
2743                    }
2744                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2745                    // `SHOW VARIABLES`. Both return a 2-column row
2746                    // set listing server-side state; clients probe
2747                    // them at connect time.
2748                    "status" => Ok(Statement::ShowStatus),
2749                    "variables" => Ok(Statement::ShowVariables),
2750                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2751                    "processlist" => Ok(Statement::ShowProcesslist),
2752                    "create" => {
2753                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2754                        // TABLE is supported in v7.17.
2755                        let kind = match self.advance() {
2756                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2757                            Token::Table => "table".to_string(),
2758                            other => {
2759                                return Err(self.err(format!(
2760                                    "expected TABLE after SHOW CREATE, got {other:?}"
2761                                )));
2762                            }
2763                        };
2764                        if !kind.eq_ignore_ascii_case("table") {
2765                            return Err(self.err(format!(
2766                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2767                            )));
2768                        }
2769                        let name = self.expect_ident_like()?;
2770                        Ok(Statement::ShowCreateTable(name))
2771                    }
2772                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2773                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2774                    // it to populate the database selector at connect
2775                    // time; without it `mysql -p` errors before the
2776                    // first user query.
2777                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2778                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2779                    // keyword on its own; it lands here as a bare
2780                    // ident. Returning all publications + their
2781                    // scope summary.
2782                    "publications" => Ok(Statement::ShowPublications),
2783                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2784                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2785                    "columns" => {
2786                        if !matches!(self.peek(), Token::From) {
2787                            return Err(self.err(format!(
2788                                "expected FROM after SHOW COLUMNS, got {:?}",
2789                                self.peek()
2790                            )));
2791                        }
2792                        self.advance();
2793                        let table = self.expect_ident_like()?;
2794                        Ok(Statement::ShowColumns(table))
2795                    }
2796                    // v7.38 轴 4 surface — `SHOW <param>` for any
2797                    // remaining session / preset parameter name
2798                    // (server_version, search_path, client_encoding,
2799                    // …). The engine's ShowParameter handler does the
2800                    // dispatch; unrecognised names error there with
2801                    // a pointer to pg_settings, not at parse time —
2802                    // so a driver that issues `SHOW spam_setting`
2803                    // gets a clear runtime error instead of a
2804                    // confusing "unknown SHOW target".
2805                    other => {
2806                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2807                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2808                        // consume the dotted tail so it round-trips with
2809                        // `SET app.foo` / `current_setting('app.foo')`.
2810                        let mut full = other.to_string();
2811                        while matches!(self.peek(), Token::Dot) {
2812                            self.advance();
2813                            let seg = self.expect_ident_like()?;
2814                            full.push('.');
2815                            full.push_str(&seg.to_ascii_lowercase());
2816                        }
2817                        Ok(Statement::ShowParameter(full))
2818                    }
2819                }
2820            }
2821            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2822            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2823            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2824            // arrived as a bare ident; tokenising it dedicatedly
2825            // keeps the dispatch tree small.
2826            Token::Drop => {
2827                self.advance();
2828                match self.peek() {
2829                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2830                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2831                    // around DROP ROLE cleanup. SPG has no role-owner
2832                    // model, so consume to boundary as a no-op.
2833                    Token::Ident(s) | Token::QuotedIdent(s)
2834                        if s.eq_ignore_ascii_case("owned") =>
2835                    {
2836                        // v7.39 (round 696) — still a no-op (SPG has no
2837                        // role-owner model), but the ROLE is carried out so
2838                        // the engine can refuse one that does not exist,
2839                        // which is what PG18 does.
2840                        self.advance();
2841                        if self.peek_is_by() {
2842                            self.advance();
2843                        }
2844                        let names = self.take_comma_separated_names();
2845                        self.consume_until_statement_boundary();
2846                        Ok(Statement::ValidateOnly {
2847                            kind: crate::ast::ValidateOnlyKind::RoleName,
2848                            names,
2849                        })
2850                    }
2851                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2852                    // It drops only a TEMPORARY table, and name resolution
2853                    // already prefers the session's own, so the keyword is
2854                    // consumed and the ordinary DROP TABLE path runs.
2855                    Token::Ident(s) | Token::QuotedIdent(s)
2856                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2857                    {
2858                        self.advance();
2859                        if !matches!(self.peek(), Token::Table) {
2860                            return Err(self.err(alloc::format!(
2861                                "expected TABLE after DROP TEMPORARY, got {:?}",
2862                                self.peek()
2863                            )));
2864                        }
2865                        self.parse_drop_table_after_keyword()
2866                    }
2867                    Token::Publication => {
2868                        self.advance();
2869                        // v7.39 (round 754, F31-B4) — the round-753
2870                        // audit probe tripped over the missing
2871                        // `IF EXISTS` here (syntax error).
2872                        let if_exists = self.consume_if_exists();
2873                        let name = self.expect_ident_or_string()?;
2874                        Ok(Statement::DropPublication { name, if_exists })
2875                    }
2876                    Token::Subscription => {
2877                        self.advance();
2878                        let if_exists = self.consume_if_exists();
2879                        let name = self.expect_ident_or_string()?;
2880                        Ok(Statement::DropSubscription { name, if_exists })
2881                    }
2882                    Token::Ident(s) | Token::QuotedIdent(s)
2883                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2884                    {
2885                        self.advance();
2886                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2887                        // login user IS a role in PG, and SPG's store holds
2888                        // both. `IF EXISTS` is accepted on either spelling.
2889                        let if_exists = self.consume_if_exists();
2890                        let name = self.expect_ident_or_string()?;
2891                        Ok(Statement::DropUser { name, if_exists })
2892                    }
2893                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2894                    // CREATE DATABASE has parsed since v7.14 and this did
2895                    // not, so `DROP DATABASE IF EXISTS x` — what every
2896                    // teardown script and pg_dumpall preamble opens with —
2897                    // came back as a syntax error, which IF EXISTS cannot
2898                    // soften. The name is carried so the engine can answer
2899                    // the way PG does; PG never lets this succeed on a
2900                    // single-database server, since the name is either
2901                    // unknown ("database … does not exist", or a notice
2902                    // under IF EXISTS) or the one you are connected to
2903                    // ("cannot drop the currently open database").
2904                    Token::Ident(s) | Token::QuotedIdent(s)
2905                        if s.eq_ignore_ascii_case("database") =>
2906                    {
2907                        self.advance();
2908                        let if_exists = self.consume_if_exists();
2909                        let name = self.expect_ident_or_string()?;
2910                        self.consume_until_statement_boundary();
2911                        Ok(Statement::DropDatabase { name, if_exists })
2912                    }
2913                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2914                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2915                        self.advance();
2916                        let if_exists = self.consume_if_exists();
2917                        let name = self.expect_ident_like()?;
2918                        // ON <table>
2919                        if !matches!(self.peek(), Token::On) {
2920                            return Err(self.err(alloc::format!(
2921                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2922                                self.peek()
2923                            )));
2924                        }
2925                        self.advance();
2926                        let table = self.expect_ident_like()?;
2927                        Ok(Statement::DropTrigger {
2928                            name,
2929                            table,
2930                            if_exists,
2931                        })
2932                    }
2933                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2934                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2935                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2936                        self.advance();
2937                        let if_exists = self.consume_if_exists();
2938                        let name = self.expect_ident_like()?;
2939                        if !matches!(self.peek(), Token::On) {
2940                            return Err(self.err(alloc::format!(
2941                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
2942                                self.peek()
2943                            )));
2944                        }
2945                        self.advance();
2946                        let table = self.expect_ident_like()?;
2947                        // Optional CASCADE / RESTRICT — accepted, no effect.
2948                        self.consume_until_statement_boundary();
2949                        Ok(Statement::DropRule {
2950                            name,
2951                            table,
2952                            if_exists,
2953                        })
2954                    }
2955                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2956                    // v7.12.4 ignores any optional arg-list (signature-
2957                    // based overload disambiguation lands in v7.12.5+).
2958                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2959                        self.advance();
2960                        let if_exists = self.consume_if_exists();
2961                        let name = self.expect_ident_like()?;
2962                        // v7.39 (read01 round 62) — the argument list identifies
2963                        // WHICH overload to drop, so it is captured, not
2964                        // discarded. `DROP FUNCTION f` (no list) is legal when
2965                        // the name is unambiguous; the engine enforces that.
2966                        let args = if matches!(self.peek(), Token::LParen) {
2967                            Some(self.parse_function_signature_types()?)
2968                        } else {
2969                            None
2970                        };
2971                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2972                        // trailer, which `DROP TABLE` and `DROP INDEX` have
2973                        // accepted since v7.14 and this one refused outright.
2974                        // pg_dump writes it, so refusing was a parse error in
2975                        // the middle of a restore. SPG drops the function
2976                        // either way — it tracks no dependents to cascade to —
2977                        // which is the same reading the other two give it.
2978                        self.consume_drop_behaviour();
2979                        Ok(Statement::DropFunction {
2980                            name,
2981                            args,
2982                            if_exists,
2983                        })
2984                    }
2985                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2986                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2987                    // emit DROP TABLE IF EXISTS at the head of every
2988                    // CREATE TABLE block so re-importing a dump
2989                    // overwrites prior state. SPG accepts and removes
2990                    // matching tables; CASCADE/RESTRICT trailers
2991                    // accepted silently.
2992                    Token::Table => self.parse_drop_table_after_keyword(),
2993                    // v7.14.0 — DROP INDEX [IF EXISTS] name
2994                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
2995                    // for partial-index renames and pgvector
2996                    // migrations. SPG removes the matching index;
2997                    // IF EXISTS makes the drop idempotent.
2998                    Token::Index => {
2999                        self.advance();
3000                        let if_exists = self.consume_if_exists();
3001                        let name = self.expect_ident_like()?;
3002                        if matches!(
3003                            self.peek(),
3004                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3005                                || s.eq_ignore_ascii_case("restrict")
3006                        ) {
3007                            self.advance();
3008                        }
3009                        Ok(Statement::DropIndex { name, if_exists })
3010                    }
3011                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3012                    // [CASCADE|RESTRICT]. SPG is single-database;
3013                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3014                    // name [, name…] [CASCADE | RESTRICT]. Real
3015                    // unregister (was silent no-op pre-v7.17).
3016                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3017                        self.advance();
3018                        let if_exists = self.consume_if_exists();
3019                        let mut names = vec![self.expect_ident_like()?];
3020                        while matches!(self.peek(), Token::Comma) {
3021                            self.advance();
3022                            names.push(self.expect_ident_like()?);
3023                        }
3024                        if matches!(
3025                            self.peek(),
3026                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3027                                || s.eq_ignore_ascii_case("restrict")
3028                        ) {
3029                            self.advance();
3030                        }
3031                        Ok(Statement::DropSchema { names, if_exists })
3032                    }
3033                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3034                    // name [, name…] [CASCADE|RESTRICT].
3035                    Token::Ident(s) | Token::QuotedIdent(s)
3036                        if s.eq_ignore_ascii_case("type") =>
3037                    {
3038                        self.advance();
3039                        let if_exists = self.consume_if_exists();
3040                        let mut names = vec![self.expect_ident_like()?];
3041                        while matches!(self.peek(), Token::Comma) {
3042                            self.advance();
3043                            names.push(self.expect_ident_like()?);
3044                        }
3045                        if matches!(
3046                            self.peek(),
3047                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3048                                || s.eq_ignore_ascii_case("restrict")
3049                        ) {
3050                            self.advance();
3051                        }
3052                        Ok(Statement::DropType { names, if_exists })
3053                    }
3054                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3055                    // name [, name…] [CASCADE|RESTRICT].
3056                    Token::Ident(s) | Token::QuotedIdent(s)
3057                        if s.eq_ignore_ascii_case("domain") =>
3058                    {
3059                        self.advance();
3060                        let if_exists = self.consume_if_exists();
3061                        let mut names = vec![self.expect_ident_like()?];
3062                        while matches!(self.peek(), Token::Comma) {
3063                            self.advance();
3064                            names.push(self.expect_ident_like()?);
3065                        }
3066                        if matches!(
3067                            self.peek(),
3068                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3069                                || s.eq_ignore_ascii_case("restrict")
3070                        ) {
3071                            self.advance();
3072                        }
3073                        Ok(Statement::DropDomain { names, if_exists })
3074                    }
3075                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3076                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3077                    Token::Ident(s) | Token::QuotedIdent(s)
3078                        if s.eq_ignore_ascii_case("materialized") =>
3079                    {
3080                        self.advance();
3081                        let nxt = self.peek().clone();
3082                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3083                        {
3084                            return Err(self.err(alloc::format!(
3085                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3086                            )));
3087                        }
3088                        self.advance();
3089                        let if_exists = self.consume_if_exists();
3090                        let mut names = vec![self.expect_ident_like()?];
3091                        while matches!(self.peek(), Token::Comma) {
3092                            self.advance();
3093                            names.push(self.expect_ident_like()?);
3094                        }
3095                        if matches!(
3096                            self.peek(),
3097                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3098                                || s.eq_ignore_ascii_case("restrict")
3099                        ) {
3100                            self.advance();
3101                        }
3102                        Ok(Statement::DropMaterializedView { names, if_exists })
3103                    }
3104                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3105                    // name [, name…] [CASCADE|RESTRICT].
3106                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3107                        self.advance();
3108                        let if_exists = self.consume_if_exists();
3109                        let mut names = vec![self.expect_ident_like()?];
3110                        while matches!(self.peek(), Token::Comma) {
3111                            self.advance();
3112                            names.push(self.expect_ident_like()?);
3113                        }
3114                        if matches!(
3115                            self.peek(),
3116                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3117                                || s.eq_ignore_ascii_case("restrict")
3118                        ) {
3119                            self.advance();
3120                        }
3121                        Ok(Statement::DropView { names, if_exists })
3122                    }
3123                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3124                    // [CASCADE|RESTRICT]. Real removal from catalog
3125                    // (was a silent no-op pre-v7.17).
3126                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
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::DropSequence { names, if_exists })
3142                    }
3143                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3144                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3145                        self.advance();
3146                        self.parse_drop_policy_after_keyword()
3147                    }
3148                    // v7.37.17 (17.6 siblings) — DROP <target> for
3149                    // targets SPG doesn't natively track. pg_dump
3150                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3151                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3152                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3153                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3154                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3155                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3156                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3157                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3158                    // etc. — accept + Empty-return so pg_dump tails
3159                    // load through. Materialized-view drop dispatches
3160                    // to the existing DropTable path when the token
3161                    // is Materialized-View-shaped (elsewhere in
3162                    // this parser).
3163                    Token::Ident(s) | Token::QuotedIdent(s)
3164                        if s.eq_ignore_ascii_case("text")
3165                            // The DROP dispatch matches on PEEK — `text` is
3166                            // not yet consumed, so SEARCH/CONFIGURATION sit
3167                            // at pos+1/pos+2 (the round-695 trap's mirror).
3168                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3169                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3170                    {
3171                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3172                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3173                        // stay in the noise arm below.
3174                        self.advance(); // TEXT
3175                        self.advance(); // SEARCH
3176                        self.advance(); // CONFIGURATION
3177                        let if_exists = self.consume_if_exists();
3178                        let names = self.take_comma_separated_names();
3179                        self.consume_until_statement_boundary();
3180                        if if_exists {
3181                            return Ok(Statement::Empty);
3182                        }
3183                        Ok(Statement::ValidateOnly {
3184                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3185                            names,
3186                        })
3187                    }
3188                    Token::Ident(s) | Token::QuotedIdent(s)
3189                        if matches!(
3190                            s.to_ascii_lowercase().as_str(),
3191                            "type"
3192                                | "domain"
3193                                | "operator"
3194                                | "cast"
3195                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3196                                // TEMPLATE (CONFIGURATION intercepted above).
3197                                | "text"
3198                                | "materialized"
3199                                | "large"
3200                                | "role"
3201                                | "access"
3202                                | "procedure"
3203                                | "routine"
3204                        ) =>
3205                    {
3206                        self.consume_until_statement_boundary();
3207                        Ok(Statement::Empty)
3208                    }
3209                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3210                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3211                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3212                    // foreign-data warning family (round 706) so a
3213                    // CREATE→DROP sequence in a dump stays consistent.
3214                    Token::Ident(s) | Token::QuotedIdent(s)
3215                        if s.eq_ignore_ascii_case("server")
3216                            || s.eq_ignore_ascii_case("foreign") =>
3217                    {
3218                        self.advance();
3219                        self.consume_until_statement_boundary();
3220                        Ok(Statement::ValidateOnly {
3221                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3222                            names: Vec::new(),
3223                        })
3224                    }
3225                    Token::Ident(s) | Token::QuotedIdent(s)
3226                        if s.eq_ignore_ascii_case("collation")
3227                            || s.eq_ignore_ascii_case("tablespace") =>
3228                    {
3229                        let kind = if s.eq_ignore_ascii_case("collation") {
3230                            crate::ast::ValidateOnlyKind::CollationName
3231                        } else {
3232                            crate::ast::ValidateOnlyKind::TablespaceName
3233                        };
3234                        self.advance();
3235                        let if_exists = self.consume_if_exists();
3236                        let names = self.take_comma_separated_names();
3237                        self.consume_until_statement_boundary();
3238                        if if_exists {
3239                            return Ok(Statement::Empty);
3240                        }
3241                        Ok(Statement::ValidateOnly { kind, names })
3242                    }
3243                    Token::Ident(s) | Token::QuotedIdent(s)
3244                        if s.eq_ignore_ascii_case("event") =>
3245                    {
3246                        self.advance();
3247                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3248                        {
3249                            self.advance();
3250                        }
3251                        let if_exists = self.consume_if_exists();
3252                        let names = self.take_comma_separated_names();
3253                        self.consume_until_statement_boundary();
3254                        if if_exists {
3255                            return Ok(Statement::Empty);
3256                        }
3257                        Ok(Statement::ValidateOnly {
3258                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3259                            names,
3260                        })
3261                    }
3262                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3263                    // leave the noise list; see the ValidateOnly kinds.
3264                    Token::Ident(s) | Token::QuotedIdent(s)
3265                        if s.eq_ignore_ascii_case("conversion")
3266                            || s.eq_ignore_ascii_case("language")
3267                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3268                            // FIRST — the first draft looked for it after.
3269                            || s.eq_ignore_ascii_case("procedural") =>
3270                    {
3271                        let kind = if s.eq_ignore_ascii_case("conversion") {
3272                            crate::ast::ValidateOnlyKind::ConversionName
3273                        } else {
3274                            crate::ast::ValidateOnlyKind::LanguageName
3275                        };
3276                        self.advance();
3277                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3278                        {
3279                            self.advance();
3280                        }
3281                        let if_exists = self.consume_if_exists();
3282                        let names = self.take_comma_separated_names();
3283                        self.consume_until_statement_boundary();
3284                        if if_exists {
3285                            return Ok(Statement::Empty);
3286                        }
3287                        Ok(Statement::ValidateOnly { kind, names })
3288                    }
3289                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3290                    // name(argtypes)[, …]`. Parsed for real so the engine
3291                    // can answer as PG does; see Statement::DropAggregate.
3292                    Token::Ident(s) | Token::QuotedIdent(s)
3293                        if s.eq_ignore_ascii_case("aggregate") =>
3294                    {
3295                        self.advance();
3296                        let if_exists = self.consume_if_exists();
3297                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3298                        loop {
3299                            let name = self.expect_ident_like()?;
3300                            if !matches!(self.peek(), Token::LParen) {
3301                                return Err(self.err(alloc::format!(
3302                                    "expected argument list after DROP AGGREGATE {name}"
3303                                )));
3304                            }
3305                            self.advance();
3306                            let mut args: Vec<String> = Vec::new();
3307                            let mut star = false;
3308                            loop {
3309                                match self.peek().clone() {
3310                                    Token::RParen => {
3311                                        self.advance();
3312                                        break;
3313                                    }
3314                                    Token::Star => {
3315                                        self.advance();
3316                                        star = true;
3317                                    }
3318                                    Token::Comma => {
3319                                        self.advance();
3320                                    }
3321                                    _ => {
3322                                        // A type name may be multi-token
3323                                        // (`double precision`); glue idents
3324                                        // until , or ).
3325                                        let mut t = self.expect_ident_like()?;
3326                                        while let Token::Ident(nx) = self.peek() {
3327                                            let nx = nx.clone();
3328                                            self.advance();
3329                                            t.push(' ');
3330                                            t.push_str(&nx);
3331                                        }
3332                                        args.push(t);
3333                                    }
3334                                }
3335                            }
3336                            items.push((name, if star { None } else { Some(args) }));
3337                            if matches!(self.peek(), Token::Comma) {
3338                                self.advance();
3339                            } else {
3340                                break;
3341                            }
3342                        }
3343                        self.consume_until_statement_boundary();
3344                        Ok(Statement::DropAggregate { if_exists, items })
3345                    }
3346                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3347                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3348                    // installed; `IF EXISTS` is the spelling that says do
3349                    // not, and it keeps the no-op.
3350                    Token::Ident(s) | Token::QuotedIdent(s)
3351                        if s.eq_ignore_ascii_case("extension") =>
3352                    {
3353                        self.advance();
3354                        let if_exists = self.consume_if_exists();
3355                        let names = self.take_comma_separated_names();
3356                        self.consume_until_statement_boundary();
3357                        if if_exists {
3358                            return Ok(Statement::Empty);
3359                        }
3360                        Ok(Statement::ValidateOnly {
3361                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3362                            names,
3363                        })
3364                    }
3365                    Token::Ident(s) | Token::QuotedIdent(s)
3366                        if s.eq_ignore_ascii_case("statistics") =>
3367                    {
3368                        self.parse_drop_statistics_after_drop()
3369                    }
3370                    other => Err(self.err(format!(
3371                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3372                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3373                    ))),
3374                }
3375            }
3376            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3377            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3378            // and accepted before the view name. SPG materialised
3379            // views re-evaluate on read (always-fresh semantics), so
3380            // the CONCURRENTLY-vs-serial distinction has no runtime
3381            // effect — the refresh body does not block readers either
3382            // way. Same accept-and-no-op pattern as DETACH PARTITION
3383            // CONCURRENTLY (16.5).
3384            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3385                self.advance();
3386                let nxt = self.peek().clone();
3387                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3388                {
3389                    return Err(self.err(alloc::format!(
3390                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3391                    )));
3392                }
3393                self.advance();
3394                let nxt2 = self.peek().clone();
3395                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3396                {
3397                    return Err(self.err(alloc::format!(
3398                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3399                    )));
3400                }
3401                self.advance();
3402                // Optional CONCURRENTLY noise word — consumed without
3403                // changing semantics.
3404                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3405                {
3406                    self.advance();
3407                }
3408                let name = self.expect_ident_like()?;
3409                let with_data = self.parse_optional_with_data(true)?;
3410                Ok(Statement::RefreshMaterializedView { name, with_data })
3411            }
3412            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3413                self.advance();
3414                self.parse_update_after_keyword()
3415            }
3416            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3417            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3418            // [CASCADE | RESTRICT]. Clears every row from each named
3419            // table. Parses at the top level; the engine dispatcher
3420            // walks Statement::Truncate.
3421            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3422                self.advance();
3423                // Optional TABLE noise word — PG accepts both the reserved
3424                // token and the bare identifier spelling.
3425                if matches!(self.peek(), Token::Table)
3426                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3427                {
3428                    self.advance();
3429                }
3430                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3431                // not absorbed. The lookahead keeps a table genuinely
3432                // called `only` working: the keyword is a keyword only
3433                // when a name follows it.
3434                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3435                    if s.eq_ignore_ascii_case("only"))
3436                    && matches!(
3437                        self.tokens.get(self.pos + 1),
3438                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3439                    );
3440                if only {
3441                    self.advance();
3442                }
3443                // Table names (comma-separated).
3444                let mut tables = Vec::new();
3445                loop {
3446                    tables.push(self.expect_ident_like()?);
3447                    if matches!(self.peek(), Token::Comma) {
3448                        self.advance();
3449                        continue;
3450                    }
3451                    break;
3452                }
3453                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3454                let mut restart_identity = false;
3455                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3456                {
3457                    self.advance();
3458                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3459                    {
3460                        self.advance();
3461                        restart_identity = true;
3462                    }
3463                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3464                {
3465                    self.advance();
3466                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3467                    {
3468                        self.advance();
3469                    }
3470                }
3471                // Optional CASCADE / RESTRICT.
3472                let mut cascade = false;
3473                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3474                {
3475                    self.advance();
3476                    cascade = true;
3477                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3478                {
3479                    self.advance();
3480                }
3481                Ok(Statement::Truncate {
3482                    tables,
3483                    restart_identity,
3484                    cascade,
3485                    only,
3486                })
3487            }
3488            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3489            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3490            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3491            // rows change so the index tree is always up-to-date;
3492            // REINDEX is a strict no-op. Accept the whole statement
3493            // shape to boundary for pg_dump round-trip compatibility.
3494            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3495                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3496                // index bloat to rebuild, so the work stays a no-op, but PG
3497                // validates what it was pointed at and this swallowed the
3498                // name at parse time — `REINDEX TABLE typo` reported
3499                // success. Measured on PG18: INDEX / TABLE name a relation,
3500                // SCHEMA a schema, SYSTEM nothing.
3501                self.advance();
3502                self.parse_reindex_tail()
3503            }
3504            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3505            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3506            // SPG has no MVCC bloat today (Phase D visibility map
3507            // queues with v7.38); the freezer collapses hot-tier
3508            // rows into cold segments automatically. VACUUM is a
3509            // no-op — pg_dump maintenance scripts and Discourse's
3510            // periodic-maintenance path both emit it.
3511            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3512            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3513            // actual bloat, so the pre-MVCC accept-and-ignore posture
3514            // became a silent no-op on a customer's manual reclaim.
3515            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3516            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3517            // ANALYZE is captured, the optional table name is captured.
3518            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3519                self.advance();
3520                // Parenthesised option list: absorb it.
3521                if matches!(self.peek(), Token::LParen) {
3522                    let mut depth = 0usize;
3523                    loop {
3524                        match self.advance() {
3525                            Token::LParen => depth += 1,
3526                            Token::RParen => {
3527                                depth -= 1;
3528                                if depth == 0 {
3529                                    break;
3530                                }
3531                            }
3532                            Token::Eof => break,
3533                            _ => {}
3534                        }
3535                    }
3536                }
3537                let mut analyze = false;
3538                let mut table: Option<String> = None;
3539                loop {
3540                    match self.peek() {
3541                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3542                        // an identifier, so the loop below broke out on it and
3543                        // dropped the table name: `VACUUM FULL nosuch` was
3544                        // accepted where `VACUUM nosuch` was refused.
3545                        Token::Full => {
3546                            self.advance();
3547                        }
3548                        Token::Ident(w) | Token::QuotedIdent(w) => {
3549                            let wl = w.to_ascii_lowercase();
3550                            match wl.as_str() {
3551                                "full" | "freeze" | "verbose" => {
3552                                    self.advance();
3553                                }
3554                                "analyze" | "analyse" => {
3555                                    analyze = true;
3556                                    self.advance();
3557                                }
3558                                _ => {
3559                                    table = Some(self.expect_ident_like()?);
3560                                    break;
3561                                }
3562                            }
3563                        }
3564                        _ => break,
3565                    }
3566                }
3567                // Optional trailing column list / anything else to the
3568                // statement boundary (PG accepts per-column ANALYZE).
3569                self.consume_until_statement_boundary();
3570                Ok(Statement::Vacuum { table, analyze })
3571            }
3572            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3573            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3574            // <index>. PG stores rows in physical order matching
3575            // an index; SPG's hot-tier is append-only + cold-tier
3576            // is segment-frozen, so clustering has no persistent
3577            // effect. Accept-and-no-op for pg_dump compat.
3578            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3579                // v7.39 (round 535) — same as REINDEX above: the relation is
3580                // carried so the engine can refuse one that does not exist.
3581                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3582                self.advance();
3583                self.parse_cluster_tail()
3584            }
3585            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3586            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3587            // optional string payload; UNLISTEN takes a channel or `*`.
3588            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3589                self.advance();
3590                let ch = match self.advance() {
3591                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3592                    other => {
3593                        return Err(self.err(format!(
3594                            "expected channel name after LISTEN, got {other:?}"
3595                        )));
3596                    }
3597                };
3598                Ok(Statement::Listen(ch))
3599            }
3600            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3601                self.advance();
3602                let channel = match self.advance() {
3603                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3604                    other => {
3605                        return Err(self.err(format!(
3606                            "expected channel name after NOTIFY, got {other:?}"
3607                        )));
3608                    }
3609                };
3610                let payload = if matches!(self.peek(), Token::Comma) {
3611                    self.advance();
3612                    match self.advance() {
3613                        Token::String(p) => Some(p),
3614                        other => {
3615                            return Err(self.err(format!(
3616                                "expected string payload after NOTIFY <channel>, got {other:?}"
3617                            )));
3618                        }
3619                    }
3620                } else {
3621                    None
3622                };
3623                Ok(Statement::Notify { channel, payload })
3624            }
3625            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3626                self.advance();
3627                match self.advance() {
3628                    Token::Star => Ok(Statement::Unlisten(None)),
3629                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3630                    other => Err(self.err(format!(
3631                        "expected channel name or * after UNLISTEN, got {other:?}"
3632                    ))),
3633                }
3634            }
3635            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3636            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3637            // process-wide write lock today; explicit LOCK has no
3638            // effect. Accept-and-no-op for pg_dump / migration
3639            // compat.
3640            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3641                self.advance();
3642                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3643                // engine holds a process-wide write lock), but the TABLE
3644                // NAME is now carried out so the engine can refuse one that
3645                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3646                // READ|WRITE` is a different statement with the same first
3647                // word; it keeps the old no-op, because a MySQL dump's
3648                // bracket names tables it is about to create.
3649                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3650                    if k.eq_ignore_ascii_case("tables"));
3651                if mysql_tables {
3652                    self.consume_until_statement_boundary();
3653                    return Ok(Statement::Empty);
3654                }
3655                if matches!(self.peek(), Token::Table) {
3656                    self.advance();
3657                }
3658                let names = self.take_comma_separated_names();
3659                self.consume_until_statement_boundary();
3660                Ok(Statement::ValidateOnly {
3661                    kind: crate::ast::ValidateOnlyKind::LockTable,
3662                    names,
3663                })
3664            }
3665            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3666            // durability marker + snapshot in PG. SPG has WAL
3667            // checkpointing on a byte / time schedule (v7.37.10
3668            // 60s / 4 MiB defaults). The bare statement parses to
3669            // `Statement::Empty` here (the no_std engine owns no
3670            // WAL / snapshot); v7.37 Epic Du wires the HOST
3671            // (embedded `Database::execute_buffered`, via
3672            // `sql_is_checkpoint`) to force an immediate synchronous
3673            // checkpoint through `Database::checkpoint` — a real
3674            // durability barrier, matching PG.
3675            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3676                self.advance();
3677                self.consume_until_statement_boundary();
3678                Ok(Statement::Empty)
3679            }
3680            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3681                self.advance();
3682                self.parse_delete_after_keyword()
3683            }
3684            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3685            // ALTER is not a reserved keyword in the lexer — handled
3686            // as a bare ident here.
3687            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3688                self.advance();
3689                self.parse_alter_after_keyword()
3690            }
3691            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3692            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3693            // additions needed.
3694            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3695                self.advance();
3696                self.parse_wait_after_keyword()
3697            }
3698            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3699            // Bare ANALYZE → analyse every user table; ANALYZE
3700            // <name> → re-stats one. The argument is an optional
3701            // ident (or quoted ident); anything else is a parse
3702            // error.
3703            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3704            // `WHERE` filter (carved out per V6_7_DESIGN.md
3705            // STABILITY). Lex order: identifier "compact" → "cold"
3706            // → "segments". Anything else after `COMPACT` is a
3707            // parse error.
3708            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3709                self.advance();
3710                let next = self.peek().clone();
3711                let cold = match next {
3712                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3713                    _ => {
3714                        return Err(
3715                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3716                        );
3717                    }
3718                };
3719                if !cold.eq_ignore_ascii_case("cold") {
3720                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3721                }
3722                self.advance();
3723                let next = self.peek().clone();
3724                let segments = match next {
3725                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3726                    _ => {
3727                        return Err(self.err(format!(
3728                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3729                            self.peek()
3730                        )));
3731                    }
3732                };
3733                if !segments.eq_ignore_ascii_case("segments") {
3734                    return Err(self.err(format!(
3735                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3736                    )));
3737                }
3738                self.advance();
3739                Ok(Statement::CompactColdSegments)
3740            }
3741            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3742            // Parsed as a case-insensitive identifier since MERGE
3743            // isn't a reserved lexer keyword (collides with the
3744            // mysqldump `ALGORITHM = MERGE` view clause if it
3745            // were); the inner parser drives the rest of the
3746            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3747            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3748                self.advance();
3749                self.parse_merge_after_keyword()
3750            }
3751            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3752                self.advance();
3753                let target = match self.peek() {
3754                    Token::Eof | Token::Semicolon => None,
3755                    Token::Ident(_) | Token::QuotedIdent(_) => {
3756                        Some(self.expect_ident_like()?)
3757                    }
3758                    other => {
3759                        return Err(self.err(format!(
3760                            "expected table name or end of statement after ANALYZE, got {other:?}"
3761                        )));
3762                    }
3763                };
3764                // v7.39 (round 776, F31 J7) — the per-column form
3765                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3766                // here while the VACUUM arm already consumed it; SPG
3767                // analyzes whole tables, so the list parses and is
3768                // accepted like the VACUUM path's.
3769                if target.is_some() && matches!(self.peek(), Token::LParen) {
3770                    self.advance();
3771                    loop {
3772                        let _ = self.expect_ident_like()?;
3773                        match self.peek() {
3774                            Token::Comma => {
3775                                self.advance();
3776                            }
3777                            Token::RParen => {
3778                                self.advance();
3779                                break;
3780                            }
3781                            other => {
3782                                return Err(self.err(format!(
3783                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3784                                )));
3785                            }
3786                        }
3787                    }
3788                }
3789                Ok(Statement::Analyze(target))
3790            }
3791            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3792            // `default_text_search_config` parameter is consumed
3793            // by the FTS function dispatcher; other parameter
3794            // names are recorded but treated as a no-op so PG
3795            // dump output loads.
3796            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3797                self.advance();
3798                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3799                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3800                // …` which the SessionVar path handles). `LOCAL` is the only
3801                // one that changes semantics — it scopes the change to the
3802                // current transaction — so capture it; SESSION / GLOBAL are
3803                // accepted and treated as the default session scope.
3804                let mut set_local = false;
3805                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3806                    let q = s.to_ascii_lowercase();
3807                    if q == "local" || q == "session" || q == "global" {
3808                        set_local = q == "local";
3809                        self.advance();
3810                    }
3811                }
3812                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3813                // <collation>]` — change the connection client
3814                // charset. SPG stores UTF-8 always and orders
3815                // bytewise; accept as a no-op.
3816                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3817                {
3818                    self.advance();
3819                    // Charset ident-or-string.
3820                    if matches!(
3821                        self.peek(),
3822                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3823                    ) {
3824                        self.advance();
3825                    }
3826                    // Optional `COLLATE <name>`.
3827                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3828                    {
3829                        self.advance();
3830                        if matches!(
3831                            self.peek(),
3832                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3833                        ) {
3834                            self.advance();
3835                        }
3836                    }
3837                    return Ok(Statement::Empty);
3838                }
3839                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3840                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3841                // uses this to switch to the object owner before
3842                // recreating tables. SPG has no role system so this
3843                // is a no-op.
3844                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3845                {
3846                    self.advance(); // ROLE
3847                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3848                    // reset to the login identity; a name / string sets the
3849                    // effective role that drives current_user + RLS.
3850                    let role = match self.peek().clone() {
3851                        Token::Default => {
3852                            self.advance();
3853                            None
3854                        }
3855                        Token::Ident(s) | Token::QuotedIdent(s)
3856                            if s.eq_ignore_ascii_case("none") =>
3857                        {
3858                            self.advance();
3859                            None
3860                        }
3861                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3862                            self.advance();
3863                            Some(s)
3864                        }
3865                        _ => None,
3866                    };
3867                    return Ok(Statement::SetRole(role));
3868                }
3869                // v7.37.17 (17.6 sibling) — PG `SET SESSION
3870                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3871                // ISO SQL surface). pg_dump prepends this to fix
3872                // the isolation level for the restore session. SPG
3873                // defaults to READ COMMITTED and doesn't yet honor
3874                // session-set isolation across statements — accept
3875                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3876                // per-tx form is handled elsewhere.
3877                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3878                {
3879                    self.advance(); // CHARACTERISTICS
3880                    self.consume_until_statement_boundary();
3881                    return Ok(Statement::Empty);
3882                }
3883                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3884                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3885                // pg_dump emits this to control the deferrability of
3886                // FK / UNIQUE constraints across a bulk restore. SPG
3887                // has no deferrable-constraint machinery today; the
3888                // FK checker is strict-immediate. Accept-and-no-op
3889                // for pg_dump round-trip compatibility.
3890                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3891                {
3892                    self.advance(); // CONSTRAINTS
3893                    // v7.39 (round 288) — no longer a no-op: the trailing
3894                    // DEFERRED / IMMEDIATE sets the transaction's timing.
3895                    // v7.39 (round 308, V29) — and the names are kept.
3896                    // They used to be skipped over on the way to the
3897                    // DEFERRED keyword, so a named form silently behaved
3898                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3899                    // every deferrable constraint in the transaction.
3900                    let mut names: alloc::vec::Vec<alloc::string::String> =
3901                        alloc::vec::Vec::new();
3902                    if matches!(self.peek(), Token::All) {
3903                        self.advance();
3904                    } else {
3905                        loop {
3906                            let mut n = self.expect_ident_like()?;
3907                            // A schema-qualified name (`public.fk_a`)
3908                            // identifies the same constraint; PG resolves
3909                            // it by the trailing segment.
3910                            while matches!(self.peek(), Token::Dot) {
3911                                self.advance();
3912                                n = self.expect_ident_like()?;
3913                            }
3914                            names.push(n);
3915                            if matches!(self.peek(), Token::Comma) {
3916                                self.advance();
3917                            } else {
3918                                break;
3919                            }
3920                        }
3921                    }
3922                    let deferred = match self.peek() {
3923                        Token::Ident(s) | Token::QuotedIdent(s)
3924                            if s.eq_ignore_ascii_case("deferred") =>
3925                        {
3926                            true
3927                        }
3928                        Token::Ident(s) | Token::QuotedIdent(s)
3929                            if s.eq_ignore_ascii_case("immediate") =>
3930                        {
3931                            false
3932                        }
3933                        other => {
3934                            return Err(self.err(alloc::format!(
3935                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3936                            )));
3937                        }
3938                    };
3939                    self.advance();
3940                    return Ok(Statement::SetConstraints { names, deferred });
3941                }
3942                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3943                // { DEFAULT | '<role>' | <ident> }` (mailrs
3944                // round-10 A.1). pg_dump preamble emits the
3945                // `DEFAULT` form to reset session authorization.
3946                //
3947                // v7.39 (round 697) — this said "SPG has no role system so
3948                // this is a strict no-op". SPG has had one since round 58;
3949                // the comment outlived it, and with it the reason a name
3950                // that is not a role was accepted here. It still switches
3951                // no authorization — what it does now is refuse a role
3952                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3953                // AUTHORIZATION` (handled by the RESET parser
3954                // elsewhere). Reference:
3955                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3956                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3957                {
3958                    self.advance(); // AUTHORIZATION
3959                    match self.peek().clone() {
3960                        Token::Default => {
3961                            self.advance();
3962                        }
3963                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3964                            self.advance();
3965                            return Ok(Statement::ValidateOnly {
3966                                kind: crate::ast::ValidateOnlyKind::RoleName,
3967                                names: alloc::vec![r],
3968                            });
3969                        }
3970                        other => {
3971                            return Err(self.err(alloc::format!(
3972                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3973                            )));
3974                        }
3975                    }
3976                    return Ok(Statement::Empty);
3977                }
3978                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3979                // ISOLATION LEVEL { READ COMMITTED | READ
3980                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3981                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3982                // PG-standard surface. v7.37.8 accepts the syntax
3983                // and tracks the selected level on
3984                // `Engine::current_isolation_level()`; the actual
3985                // MVCC / SSI semantics implementation lands in
3986                // the 轴 4 isolation framework (separate train).
3987                // PG itself maps READ UNCOMMITTED to READ COMMITTED
3988                // internally; SPG behaves the same (effectively
3989                // READ COMMITTED at every level today).
3990                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3991                {
3992                    self.advance(); // TRANSACTION
3993                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3994                    return Ok(Statement::SetTransaction { isolation: level });
3995                }
3996                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
3997                // alias — same accept-as-no-op as SET NAMES.
3998                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
3999                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4000                {
4001                    self.advance(); // CHARACTER
4002                    self.advance(); // SET
4003                    if matches!(
4004                        self.peek(),
4005                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4006                    ) {
4007                        self.advance();
4008                    }
4009                    return Ok(Statement::Empty);
4010                }
4011                // v7.39 (GUC) — PG spells the timezone GUC as two
4012                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4013                // where <value> is a string/ident or the LOCAL /
4014                // DEFAULT keyword (both mean "back to the default").
4015                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4016                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4017                {
4018                    self.advance(); // TIME
4019                    self.advance(); // ZONE
4020                    let value = match self.peek().clone() {
4021                        Token::Ident(s)
4022                            if s.eq_ignore_ascii_case("local")
4023                                || s.eq_ignore_ascii_case("default") =>
4024                        {
4025                            self.advance();
4026                            crate::ast::SetValue::Default
4027                        }
4028                        Token::Default => {
4029                            self.advance();
4030                            crate::ast::SetValue::Default
4031                        }
4032                        _ => self.parse_set_value()?,
4033                    };
4034                    return Ok(Statement::SetParameter {
4035                        name: "timezone".into(),
4036                        value,
4037                        local: set_local,
4038                    });
4039                }
4040                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4041                // MySQL USER-variable assignment: its own per-session
4042                // namespace, an arbitrary expression on the right, and `:=`
4043                // as a second spelling of `=`. It used to fall into the
4044                // session-PARAMETER list below, whose values are literals and
4045                // whose store nothing reads back under a `@` name — so the
4046                // assignment reported success and vanished.
4047                //
4048                // A `@@`-prefixed LHS is a real engine setting and keeps the
4049                // old path.
4050                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4051                    return self.parse_set_user_vars();
4052                }
4053                // v7.14.0 — multi-assignment form
4054                // `SET a = 1, b = 2, …`. Single-assignment is the
4055                // 1-element case. Each LHS may be a regular ident
4056                // or a SessionVar (`@VAR` / `@@VAR`).
4057                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4058                loop {
4059                    let lhs = match self.peek().clone() {
4060                        Token::SessionVar(s) => {
4061                            self.advance();
4062                            s
4063                        }
4064                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4065                        other => {
4066                            return Err(self.err(format!(
4067                                "expected parameter name after SET, got {other:?}"
4068                            )));
4069                        }
4070                    };
4071                    // Accept either `=` or the bare `TO` keyword.
4072                    match self.peek() {
4073                        Token::Eq => {
4074                            self.advance();
4075                        }
4076                        Token::To => {
4077                            self.advance();
4078                        }
4079                        other => {
4080                            return Err(self.err(format!(
4081                                "expected `=` or TO after SET {lhs}, got {other:?}"
4082                            )));
4083                        }
4084                    }
4085                    let mut value = self.parse_set_value()?;
4086                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4087                    // `, name TO` continues a MySQL-style multi-assign,
4088                    // anything else is a PG list VALUE
4089                    // (`SET search_path = myschema, public`) folded into
4090                    // one comma-joined string.
4091                    while matches!(self.peek(), Token::Comma) {
4092                        let is_assign = matches!(
4093                            self.tokens.get(self.pos + 1),
4094                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4095                        ) && matches!(
4096                            self.tokens.get(self.pos + 2),
4097                            Some(Token::Eq | Token::To)
4098                        );
4099                        if is_assign {
4100                            break;
4101                        }
4102                        self.advance(); // comma
4103                        let next = self.parse_set_value()?;
4104                        let joined = alloc::format!(
4105                            "{}, {}",
4106                            set_value_text(&value),
4107                            set_value_text(&next)
4108                        );
4109                        value = crate::ast::SetValue::String(joined);
4110                    }
4111                    pairs.push((lhs, value));
4112                    if matches!(self.peek(), Token::Comma) {
4113                        self.advance();
4114                        continue;
4115                    }
4116                    break;
4117                }
4118                if pairs.len() == 1 {
4119                    let (name, value) = pairs.into_iter().next().unwrap();
4120                    Ok(Statement::SetParameter {
4121                        name,
4122                        value,
4123                        local: set_local,
4124                    })
4125                } else {
4126                    Ok(Statement::SetParameterList(pairs))
4127                }
4128            }
4129            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4130            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4131                self.advance();
4132                match self.peek().clone() {
4133                    Token::All => {
4134                        self.advance();
4135                        Ok(Statement::ResetParameter(None))
4136                    }
4137                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4138                        self.advance();
4139                        Ok(Statement::ResetParameter(None))
4140                    }
4141                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4142                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4143                        self.advance();
4144                        Ok(Statement::SetRole(None))
4145                    }
4146                    _ => {
4147                        let name = self.parse_set_param_name()?;
4148                        Ok(Statement::ResetParameter(Some(name)))
4149                    }
4150                }
4151            }
4152            // v7.39 (round 218) — server-side cursors.
4153            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4154            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4155            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4156            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4157                self.advance();
4158                match self.peek().clone() {
4159                    Token::All => {
4160                        self.advance();
4161                        Ok(Statement::CloseCursor { name: None })
4162                    }
4163                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4164                        self.advance();
4165                        Ok(Statement::CloseCursor { name: None })
4166                    }
4167                    Token::Ident(n) | Token::QuotedIdent(n) => {
4168                        self.advance();
4169                        Ok(Statement::CloseCursor { name: Some(n) })
4170                    }
4171                    other => Err(self.err(format!(
4172                        "expected cursor name or ALL after CLOSE, got {other:?}"
4173                    ))),
4174                }
4175            }
4176            other => Err(self.err(format!(
4177                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4178                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4179            ))),
4180        }
4181    }
4182
4183    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4184    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4185    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4186    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4187    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4188        self.advance(); // DECLARE
4189        let name = match self.advance() {
4190            Token::Ident(n) | Token::QuotedIdent(n) => n,
4191            other => {
4192                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4193            }
4194        };
4195        let mut scroll: Option<bool> = None;
4196        loop {
4197            match self.peek() {
4198                Token::Ident(s)
4199                    if s.eq_ignore_ascii_case("binary")
4200                        || s.eq_ignore_ascii_case("insensitive")
4201                        || s.eq_ignore_ascii_case("asensitive") =>
4202                {
4203                    self.advance();
4204                }
4205                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4206                    self.advance();
4207                    scroll = Some(true);
4208                }
4209                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4210                {
4211                    self.advance(); // NO
4212                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4213                        return Err(self.err(format!(
4214                            "expected SCROLL after NO in DECLARE, got {:?}",
4215                            self.peek()
4216                        )));
4217                    }
4218                    self.advance();
4219                    scroll = Some(false);
4220                }
4221                _ => break,
4222            }
4223        }
4224        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4225            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4226        }
4227        self.advance();
4228        let mut hold = false;
4229        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4230            self.advance();
4231            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4232                return Err(self.err(format!(
4233                    "expected HOLD after WITH in DECLARE, got {:?}",
4234                    self.peek()
4235                )));
4236            }
4237            self.advance();
4238            hold = true;
4239        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4240            self.advance();
4241            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4242                return Err(self.err(format!(
4243                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4244                    self.peek()
4245                )));
4246            }
4247            self.advance();
4248        }
4249        if !matches!(self.peek(), Token::For) {
4250            return Err(self.err(format!(
4251                "expected FOR before the cursor query, got {:?}",
4252                self.peek()
4253            )));
4254        }
4255        self.advance();
4256        let query = self.parse_one_statement()?;
4257        Ok(Statement::DeclareCursor {
4258            name,
4259            scroll,
4260            hold,
4261            query: alloc::boxed::Box::new(query),
4262        })
4263    }
4264
4265    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4266    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4267    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4268    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4269        use crate::ast::CursorDirection as D;
4270        self.advance(); // FETCH / MOVE
4271        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4272            let neg = if matches!(this.peek(), Token::Minus) {
4273                this.advance();
4274                true
4275            } else {
4276                false
4277            };
4278            match this.advance() {
4279                Token::Integer(v) => Ok(if neg { -v } else { v }),
4280                other => Err(this.err(format!("expected count, got {other:?}"))),
4281            }
4282        };
4283        let direction = match self.peek().clone() {
4284            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4285                self.advance();
4286                D::Next
4287            }
4288            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4289                self.advance();
4290                D::Prior
4291            }
4292            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4293                self.advance();
4294                D::First
4295            }
4296            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4297                self.advance();
4298                D::Last
4299            }
4300            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4301                self.advance();
4302                D::Absolute(signed_count(self)?)
4303            }
4304            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4305                self.advance();
4306                D::Relative(signed_count(self)?)
4307            }
4308            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4309                self.advance();
4310                match self.peek().clone() {
4311                    Token::All => {
4312                        self.advance();
4313                        D::All
4314                    }
4315                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4316                        self.advance();
4317                        D::All
4318                    }
4319                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4320                    _ => D::Next, // bare FORWARD = FORWARD 1
4321                }
4322            }
4323            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4324                self.advance();
4325                match self.peek().clone() {
4326                    Token::All => {
4327                        self.advance();
4328                        D::BackwardAll
4329                    }
4330                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4331                        self.advance();
4332                        D::BackwardAll
4333                    }
4334                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4335                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4336                }
4337            }
4338            Token::All => {
4339                self.advance();
4340                D::All
4341            }
4342            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4343                self.advance();
4344                D::All
4345            }
4346            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4347            // Bare `FETCH <name>` — direction defaults to NEXT.
4348            _ => D::Next,
4349        };
4350        // Optional FROM / IN.
4351        if matches!(self.peek(), Token::From)
4352            || matches!(self.peek(), Token::In)
4353            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4354        {
4355            self.advance();
4356        }
4357        let name = match self.advance() {
4358            Token::Ident(n) | Token::QuotedIdent(n) => n,
4359            other => {
4360                return Err(self.err(format!("expected cursor name, got {other:?}")));
4361            }
4362        };
4363        Ok(if is_move {
4364            Statement::MoveCursor { name, direction }
4365        } else {
4366            Statement::FetchCursor { name, direction }
4367        })
4368    }
4369
4370    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4371    /// [(kind, …)] ON <col>, … FROM <table>`.
4372    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4373        self.advance(); // STATISTICS
4374        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4375        let mut if_not_exists = false;
4376        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4377            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4378        {
4379            self.advance();
4380            self.advance();
4381            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4382                self.advance();
4383                if_not_exists = true;
4384            }
4385        }
4386        let name = self.expect_ident_like()?;
4387        let mut kinds = Vec::new();
4388        if matches!(self.peek(), Token::LParen) {
4389            self.advance();
4390            loop {
4391                let k = self.expect_ident_like()?;
4392                // PG stores the single letters; accept the spelled-out
4393                // names the SQL uses and record what PG records.
4394                kinds.push(match k.to_ascii_lowercase().as_str() {
4395                    "ndistinct" => String::from("d"),
4396                    "dependencies" => String::from("f"),
4397                    "mcv" => String::from("m"),
4398                    other => {
4399                        return Err(
4400                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4401                        );
4402                    }
4403                });
4404                match self.advance() {
4405                    Token::Comma => {}
4406                    Token::RParen => break,
4407                    other => {
4408                        return Err(self.err(alloc::format!(
4409                            "expected ',' or ')' in statistics kind list, got {other:?}"
4410                        )));
4411                    }
4412                }
4413            }
4414        }
4415        if !matches!(self.peek(), Token::On) {
4416            return Err(self.err(alloc::format!(
4417                "expected ON in CREATE STATISTICS, got {:?}",
4418                self.peek()
4419            )));
4420        }
4421        self.advance();
4422        let mut columns = Vec::new();
4423        loop {
4424            columns.push(self.expect_ident_like()?);
4425            if matches!(self.peek(), Token::Comma) {
4426                self.advance();
4427            } else {
4428                break;
4429            }
4430        }
4431        if !matches!(self.peek(), Token::From) {
4432            return Err(self.err(alloc::format!(
4433                "expected FROM in CREATE STATISTICS, got {:?}",
4434                self.peek()
4435            )));
4436        }
4437        self.advance();
4438        let table = self.expect_ident_like()?;
4439        Ok(Statement::CreateStatistics {
4440            name,
4441            if_not_exists,
4442            kinds,
4443            columns,
4444            table,
4445        })
4446    }
4447
4448    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4449    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4450    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4451    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4452    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4453    /// forward call.
4454    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4455        self.advance(); // TABLE
4456        let if_exists = self.consume_if_exists();
4457        let mut names: Vec<String> = Vec::new();
4458        loop {
4459            names.push(self.expect_ident_like()?);
4460            if matches!(self.peek(), Token::Comma) {
4461                self.advance();
4462                continue;
4463            }
4464            break;
4465        }
4466        if matches!(
4467            self.peek(),
4468            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4469                || s.eq_ignore_ascii_case("restrict")
4470        ) {
4471            self.advance();
4472        }
4473        Ok(Statement::DropTable { names, if_exists })
4474    }
4475
4476    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4477        self.advance(); // STATISTICS
4478        let mut if_exists = false;
4479        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4480            && matches!(self.tokens.get(self.pos + 1),
4481                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4482        {
4483            self.advance();
4484            self.advance();
4485            if_exists = true;
4486        }
4487        let name = self.expect_ident_like()?;
4488        Ok(Statement::DropStatistics { name, if_exists })
4489    }
4490
4491    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4492        debug_assert!(matches!(self.peek(), Token::Create));
4493        self.advance();
4494        match self.peek() {
4495            Token::Table => self.parse_create_table_stmt_after_create(),
4496            Token::Index => self.parse_create_index_stmt_after_create(false),
4497            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4498            // object now. It used to be consumed by the CREATE-noise
4499            // arm, so a pg_dump that declares extended statistics
4500            // restored silently without them and reflection showed
4501            // nothing.
4502            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4503                self.parse_create_statistics_after_create()
4504            }
4505            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4506            // The `UNIQUE` modifier turns a partial index into a
4507            // partial-uniqueness invariant (only rows matching the
4508            // WHERE predicate are checked for duplicates). mailrs
4509            // K1 (3 hits: email_templates default, calendar_events
4510            // master, calendar_events instance).
4511            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4512                self.advance();
4513                if !matches!(self.peek(), Token::Index) {
4514                    return Err(self.err(alloc::format!(
4515                        "expected INDEX after CREATE UNIQUE, got {:?}",
4516                        self.peek()
4517                    )));
4518                }
4519                self.parse_create_index_stmt_after_create(true)
4520            }
4521            Token::Publication => {
4522                self.advance();
4523                self.parse_create_publication_after_keyword()
4524            }
4525            Token::Subscription => {
4526                self.advance();
4527                self.parse_create_subscription_after_keyword()
4528            }
4529            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4530            // USER isn't a reserved keyword — we look for the bare
4531            // identifier so the lexer doesn't have to grow a token.
4532            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4533                self.advance();
4534                self.parse_create_user_after_keyword(true)
4535            }
4536            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4537            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4538            // the default of the LOGIN attribute.
4539            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4540                self.advance();
4541                self.parse_create_user_after_keyword(false)
4542            }
4543            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4544            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4545                self.advance();
4546                self.parse_create_policy_after_keyword()
4547            }
4548            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4549            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4550            // no-op. mailrs follow-up F3.
4551            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4552                self.advance();
4553                self.parse_create_extension_after_keyword()
4554            }
4555            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4556            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4557            // optional; absorb it here and forward to the
4558            // per-kind parsers with the flag. OR is a reserved
4559            // keyword token.
4560            Token::Or => {
4561                self.advance();
4562                let next = self.peek();
4563                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4564                    return Err(self.err(alloc::format!(
4565                        "expected REPLACE after CREATE OR, got {next:?}"
4566                    )));
4567                };
4568                if !s2.eq_ignore_ascii_case("replace") {
4569                    return Err(self.err(alloc::format!(
4570                        "expected REPLACE after CREATE OR, got {s2:?}"
4571                    )));
4572                }
4573                self.advance();
4574                self.parse_create_function_or_trigger_after_or_replace(true)
4575            }
4576            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4577                self.advance();
4578                self.parse_create_function_after_keyword(false)
4579            }
4580            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4581                self.advance();
4582                self.parse_create_trigger_after_keyword(false)
4583            }
4584            // v7.39 (round 139) — CREATE RULE …
4585            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4586                self.advance();
4587                self.parse_create_rule_after_keyword(false)
4588            }
4589            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4590            // trigger is a row-level AFTER trigger that additionally carries
4591            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4592            // path already tolerates and skips those clauses, so consuming the
4593            // CONSTRAINT keyword and reusing it makes the statement parse and the
4594            // trigger fire. (The deferral timing itself is not yet honoured —
4595            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4596            // for every non-deferred use.)
4597            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4598                self.advance();
4599                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4600                    if t.eq_ignore_ascii_case("trigger"))
4601                {
4602                    return Err(self.err(alloc::format!(
4603                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4604                        self.peek()
4605                    )));
4606                }
4607                self.advance();
4608                self.parse_create_trigger_after_keyword(false)
4609            }
4610            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4611            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4612                self.advance();
4613                self.parse_create_sequence_after_keyword(false)
4614            }
4615            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4616            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4617                self.advance();
4618                self.parse_create_view_after_keyword(false, false, false)
4619            }
4620            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4621            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4622            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4623            // appear (in any order) between `CREATE` and `VIEW` in
4624            // every mysqldump-emitted view. Pre-2.6 the parser
4625            // rejected the prefix and the customer's whole view
4626            // backup failed on the first view. The hints are pure
4627            // planner / permission metadata; SPG's view-rewrite
4628            // path is semantically equivalent for all three
4629            // algorithms in v7.17 (TEMPTABLE differs only in
4630            // perf for huge views — out of v7.17 scope), and
4631            // DEFINER / SQL SECURITY are pure single-user
4632            // permissioning that SPG ignores by design.
4633            Token::Ident(s) | Token::QuotedIdent(s)
4634                if s.eq_ignore_ascii_case("algorithm")
4635                    || s.eq_ignore_ascii_case("definer")
4636                    || s.eq_ignore_ascii_case("sql") =>
4637            {
4638                self.consume_mysql_view_prefix()?;
4639                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4640                // (in any order, in any combination), the next
4641                // keyword must be VIEW. mysqldump never emits these
4642                // prefixes on non-view statements.
4643                let next = self.peek().clone();
4644                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4645                    if s2.eq_ignore_ascii_case("view"))
4646                {
4647                    self.advance();
4648                    self.parse_create_view_after_keyword(false, false, false)
4649                } else {
4650                    Err(self.err(alloc::format!(
4651                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4652                    )))
4653                }
4654            }
4655            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4656            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4657                self.advance();
4658                self.parse_create_type_after_keyword()
4659            }
4660            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4661            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4662            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4663                self.advance();
4664                self.parse_create_domain_after_keyword()
4665            }
4666            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4667            // name [AUTHORIZATION user]. Real catalog registry
4668            // (was silent-no-op'd pre-v7.17).
4669            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4670                self.advance();
4671                let if_not_exists = self.parse_if_not_exists();
4672                let name = self.expect_ident_like()?;
4673                // Optional `AUTHORIZATION <user>` trailer — accepted,
4674                // ignored (single-user catalog).
4675                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4676                    if s.eq_ignore_ascii_case("authorization"))
4677                {
4678                    self.advance();
4679                    let _ = self.expect_ident_like()?;
4680                }
4681                Ok(Statement::CreateSchema { name, if_not_exists })
4682            }
4683            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4684            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4685                self.advance();
4686                let next = self.peek().clone();
4687                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4688                {
4689                    self.advance();
4690                    self.parse_create_materialized_view_after_keyword()
4691                } else {
4692                    Err(self.err(alloc::format!(
4693                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4694                    )))
4695                }
4696            }
4697            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4698            // no-op below), an UNLOGGED table is a real, fully-usable table in
4699            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4700            // durability optimisation is a follow-up), so a dump / app that
4701            // declares UNLOGGED tables works instead of failing to parse.
4702            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4703                self.advance(); // UNLOGGED
4704                if matches!(self.peek(), Token::Table) {
4705                    self.parse_create_table_stmt_after_create()
4706                } else {
4707                    Err(self.err(format!(
4708                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4709                        self.peek()
4710                    )))
4711                }
4712            }
4713            Token::Ident(s) | Token::QuotedIdent(s)
4714                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4715            {
4716                self.advance();
4717                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4718                let next = self.peek().clone();
4719                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4720                {
4721                    self.advance();
4722                    self.parse_create_sequence_after_keyword(true)
4723                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4724                {
4725                    self.advance();
4726                    self.parse_create_view_after_keyword(false, false, true)
4727                } else {
4728                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4729                    // consumed and answered OK while creating nothing, so
4730                    // every statement that touched the table afterwards failed
4731                    // with "table not found" — the DDL itself lied. It is a
4732                    // real CREATE TABLE now, marked temporary so the executor
4733                    // puts it in the session's own namespace. An optional
4734                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4735                    // is not legal, but the keyword is consumed by the
4736                    // CREATE TABLE parser itself).
4737                    let stmt = self.parse_create_table_stmt_after_create()?;
4738                    match stmt {
4739                        Statement::CreateTable(mut c) => {
4740                            c.temporary = true;
4741                            Ok(Statement::CreateTable(c))
4742                        }
4743                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4744                        // CTAS node, which needs the same session namespace.
4745                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4746                            m.temporary = true;
4747                            Ok(Statement::CreateMaterializedView(m))
4748                        }
4749                        other => Ok(other),
4750                    }
4751                }
4752            }
4753            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4754            // BEGIN <body> END`. The body may reference `@var`
4755            // session variables, SET statements, internal `;`
4756            // terminators, etc. SPG has no procedure runtime, so
4757            // consume the whole `CREATE PROCEDURE … END` block as
4758            // a no-op so mysqldump scripts that include stored
4759            // routines load through. The matching-END consumer
4760            // tracks BEGIN/END nesting depth to handle nested
4761            // BEGIN blocks correctly.
4762            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4763                self.consume_mysql_routine_body();
4764                Ok(Statement::Empty)
4765            }
4766            // v7.14.0 — pg_dump / mysqldump emit
4767            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4768            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4769            // SPG is single-schema / single-database; these have
4770            // no behavioural effect, so consume + return Empty.
4771            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4772            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4773            // moved up to real parser branches. DATABASE / ROLE /
4774            // POLICY / OPERATOR stay no-op forever
4775            // (single-database, hardcoded roles).
4776            Token::Ident(s) | Token::QuotedIdent(s)
4777                if matches!(
4778                    s.to_ascii_lowercase().as_str(),
4779                    "database"
4780                        | "role"
4781                        | "operator"
4782                        | "cast"
4783                        | "aggregate"
4784                        | "language"
4785                        | "collation"
4786                        | "conversion"
4787                        // v7.17.0 Phase 8 (audit N6) — rarely-
4788                        // emitted pg_dump shapes that should
4789                        // load through without a parser error.
4790                        // SPG has no planner statistics catalog,
4791                        // no event-trigger hooks, no foreign-
4792                        // data-wrapper infrastructure; consume
4793                        // + return Empty.
4794                        | "statistics"
4795                        | "event"
4796                        // v7.37.17 (17.6 siblings) — additional CREATE
4797                        // targets pg_dump / operator install scripts
4798                        // may emit that SPG has no matching machinery
4799                        // for. Consume + Empty-return.
4800                        | "text"
4801                        | "tablespace"
4802                        | "access"
4803                        | "large"
4804                ) =>
4805            {
4806                // DATABASE is the one member of this list PG refuses
4807                // inside a transaction block; the rest (ROLE, CAST,
4808                // TABLESPACE, …) it runs there quite happily, so only
4809                // this one is named. Still a no-op otherwise — SPG is
4810                // single-database.
4811                let is_database = s.eq_ignore_ascii_case("database");
4812                self.consume_until_statement_boundary();
4813                if is_database {
4814                    return Ok(Statement::NoOpPreventedInTransaction {
4815                        what: String::from("CREATE DATABASE"),
4816                    });
4817                }
4818                Ok(Statement::Empty)
4819            }
4820            // v7.39 (round 706) — the foreign-data family leaves the silent
4821            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4822            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4823            // FDW machinery), but the ENGINE now warns, so a restore log
4824            // says what will not function instead of reporting success.
4825            Token::Ident(s) | Token::QuotedIdent(s)
4826                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4827            {
4828                self.consume_until_statement_boundary();
4829                Ok(Statement::ValidateOnly {
4830                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4831                    names: Vec::new(),
4832                })
4833            }
4834            other => Err(self.err(format!(
4835                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4836            ))),
4837        }
4838    }
4839
4840    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4841    /// keyword decides whether we parse a function or trigger
4842    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4843    /// PROCEDURE) — those land in later releases.
4844    fn parse_create_function_or_trigger_after_or_replace(
4845        &mut self,
4846        or_replace: bool,
4847    ) -> Result<Statement, ParseError> {
4848        let tok = self.peek();
4849        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4850            return Err(self.err(alloc::format!(
4851                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4852            )));
4853        };
4854        if s.eq_ignore_ascii_case("function") {
4855            self.advance();
4856            self.parse_create_function_after_keyword(or_replace)
4857        } else if s.eq_ignore_ascii_case("trigger") {
4858            self.advance();
4859            self.parse_create_trigger_after_keyword(or_replace)
4860        } else if s.eq_ignore_ascii_case("rule") {
4861            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4862            self.advance();
4863            self.parse_create_rule_after_keyword(or_replace)
4864        } else if s.eq_ignore_ascii_case("view") {
4865            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4866            self.advance();
4867            self.parse_create_view_after_keyword(or_replace, false, false)
4868        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4869            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4870            self.advance();
4871            let nxt = self.peek().clone();
4872            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4873            {
4874                self.advance();
4875                self.parse_create_view_after_keyword(or_replace, false, true)
4876            } else {
4877                Err(self.err(alloc::format!(
4878                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4879                )))
4880            }
4881        } else {
4882            Err(self.err(alloc::format!(
4883                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4884            )))
4885        }
4886    }
4887
4888    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4889    /// SPG doesn't have a registry; pgvector / similar are
4890    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4891    /// the syntax lets dual-target schemas keep the line.
4892    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4893        // Optional `IF NOT EXISTS`.
4894        self.consume_if_not_exists();
4895        let name = self.expect_ident_like()?;
4896        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4897        // CASCADE / FROM '<v>' clauses; we don't model them.
4898        loop {
4899            match self.peek() {
4900                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4901                    self.advance();
4902                    continue;
4903                }
4904                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4905                    self.advance();
4906                    let _ = self.expect_ident_like()?;
4907                    continue;
4908                }
4909                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4910                    self.advance();
4911                    // String or ident literal.
4912                    let _ = self.advance();
4913                    continue;
4914                }
4915                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4916                    self.advance();
4917                    let _ = self.advance();
4918                    continue;
4919                }
4920                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4921                    self.advance();
4922                    continue;
4923                }
4924                _ => break,
4925            }
4926        }
4927        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4928        // nosuch` reported success and `pg_extension` then did not list it,
4929        // which is the accept-and-do-nothing shape F31 exists to find.
4930        Ok(Statement::ValidateOnly {
4931            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4932            names: alloc::vec![name],
4933        })
4934    }
4935
4936    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4937    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4938    /// already been consumed by the caller. Grammar accepted:
4939    ///
4940    ///   name `(` arg-list `)`
4941    ///   `RETURNS` return-type
4942    ///   [ `LANGUAGE` ident ]
4943    ///   `AS` $$ body $$
4944    ///   [ `LANGUAGE` ident ]
4945    ///
4946    /// Either `LANGUAGE` position is allowed; PG accepts both.
4947    fn parse_create_function_after_keyword(
4948        &mut self,
4949        or_replace: bool,
4950    ) -> Result<Statement, ParseError> {
4951        let name = self.expect_ident_like()?;
4952        // Argument list. v7.12.4 commonly sees the empty `()`
4953        // (trigger functions); typed args parse and round-trip
4954        // but the executor only invokes nullary functions.
4955        if !matches!(self.peek(), Token::LParen) {
4956            return Err(self.err(alloc::format!(
4957                "expected '(' after function name {name:?}, got {:?}",
4958                self.peek()
4959            )));
4960        }
4961        self.advance();
4962        let args = self.parse_function_arg_list()?;
4963        // RETURNS clause.
4964        let tok = self.peek();
4965        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4966            return Err(self.err(alloc::format!(
4967                "expected RETURNS after function arg list, got {tok:?}"
4968            )));
4969        };
4970        if !s.eq_ignore_ascii_case("returns") {
4971            return Err(self.err(alloc::format!(
4972                "expected RETURNS after function arg list, got {s:?}"
4973            )));
4974        }
4975        self.advance();
4976        let returns = self.parse_function_return()?;
4977        // Optional LANGUAGE clause (PG also accepts after AS — we'll
4978        // re-check after the body too).
4979        let mut language: Option<String> = self.parse_optional_language()?;
4980        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4981        // either side of the body and in any order, interleaved with
4982        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4983        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4984        // PG's own pg_dump output did not restore.
4985        let mut attrs = FunctionAttrs::default();
4986        loop {
4987            let before = self.pos;
4988            self.parse_function_attrs_into(&mut attrs)?;
4989            if language.is_none() {
4990                language = self.parse_optional_language()?;
4991            }
4992            if self.pos == before {
4993                break;
4994            }
4995        }
4996        // `AS` followed by a $$-quoted body (lexer already
4997        // collapses both `$$…$$` and `$tag$…$tag$` to a single
4998        // Token::String). AS is a reserved keyword (Token::As).
4999        if !matches!(self.peek(), Token::As) {
5000            return Err(self.err(alloc::format!(
5001                "expected AS before function body, got {:?}",
5002                self.peek()
5003            )));
5004        }
5005        self.advance();
5006        let body_text = match self.peek() {
5007            Token::String(s) => {
5008                let body = s.clone();
5009                self.advance();
5010                body
5011            }
5012            other => {
5013                return Err(self.err(alloc::format!(
5014                    "expected $$-quoted function body after AS, got {other:?}"
5015                )));
5016            }
5017        };
5018        // Trailing clauses — PG's other accepted position for both the
5019        // LANGUAGE and the attributes.
5020        loop {
5021            let before = self.pos;
5022            self.parse_function_attrs_into(&mut attrs)?;
5023            if language.is_none() {
5024                language = self.parse_optional_language()?;
5025            }
5026            if self.pos == before {
5027                break;
5028            }
5029        }
5030        let language = language.unwrap_or_else(|| String::from("sql"));
5031        // PL/pgSQL bodies get structure-parsed. Other languages
5032        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5033        // recognise) round-trip as Raw text — the executor errors
5034        // when invoked with a clear unsupported message.
5035        let body = if language.eq_ignore_ascii_case("plpgsql") {
5036            match parse_plpgsql_body(&body_text) {
5037                Ok(block) => FunctionBody::PlPgSql(block),
5038                // Best-effort: if the body parser doesn't yet
5039                // support a construct used inside, fall back to
5040                // raw — keeps `CREATE FUNCTION` itself working
5041                // (catalogue accepts), executor errors on
5042                // invocation only.
5043                Err(_) => FunctionBody::Raw(body_text),
5044            }
5045        } else {
5046            FunctionBody::Raw(body_text)
5047        };
5048        Ok(Statement::CreateFunction(CreateFunctionStatement {
5049            name,
5050            or_replace,
5051            args,
5052            returns,
5053            language,
5054            body,
5055            attrs,
5056        }))
5057    }
5058
5059    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5060    /// attribute clauses into `attrs`, stopping at the first token that
5061    /// is not one. Measured against PG 18.4, which accepts them in any
5062    /// order and on either side of the body.
5063    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5064        loop {
5065            let word = match self.peek() {
5066                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5067                // NOT LEAKPROOF — NOT is a reserved keyword token.
5068                Token::Not
5069                    if matches!(
5070                        self.tokens.get(self.pos + 1),
5071                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5072                    ) =>
5073                {
5074                    self.advance();
5075                    self.advance();
5076                    attrs.leakproof = false;
5077                    continue;
5078                }
5079                _ => return Ok(()),
5080            };
5081            match word.as_str() {
5082                "immutable" => {
5083                    self.advance();
5084                    attrs.volatility = FunctionVolatility::Immutable;
5085                }
5086                "stable" => {
5087                    self.advance();
5088                    attrs.volatility = FunctionVolatility::Stable;
5089                }
5090                "volatile" => {
5091                    self.advance();
5092                    attrs.volatility = FunctionVolatility::Volatile;
5093                }
5094                "strict" => {
5095                    self.advance();
5096                    attrs.strict = true;
5097                }
5098                "leakproof" => {
5099                    self.advance();
5100                    attrs.leakproof = true;
5101                }
5102                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5103                // spelled-out forms of STRICT and its opposite.
5104                "returns" | "called" => {
5105                    let strict = word == "returns";
5106                    let mut probe = self.pos + 1;
5107                    if strict {
5108                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5109                        // is not ours.
5110                        match self.tokens.get(probe) {
5111                            Some(Token::Null) => probe += 1,
5112                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5113                            _ => return Ok(()),
5114                        }
5115                    }
5116                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5117                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5118                    if !ok {
5119                        return Ok(());
5120                    }
5121                    probe += 1;
5122                    match self.tokens.get(probe) {
5123                        Some(Token::Null) => probe += 1,
5124                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5125                        _ => return Ok(()),
5126                    }
5127                    match self.tokens.get(probe) {
5128                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5129                        _ => return Ok(()),
5130                    }
5131                    self.pos = probe;
5132                    attrs.strict = strict;
5133                }
5134                "security" | "external" => {
5135                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5136                    let mut probe = self.pos + 1;
5137                    if word == "external" {
5138                        match self.tokens.get(probe) {
5139                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5140                                probe += 1;
5141                            }
5142                            _ => return Ok(()),
5143                        }
5144                    }
5145                    let definer = match self.tokens.get(probe) {
5146                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5147                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5148                        _ => return Ok(()),
5149                    };
5150                    self.pos = probe + 1;
5151                    attrs.security_definer = definer;
5152                }
5153                "parallel" => {
5154                    let level = match self.tokens.get(self.pos + 1) {
5155                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5156                            FunctionParallel::Safe
5157                        }
5158                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5159                            FunctionParallel::Restricted
5160                        }
5161                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5162                            FunctionParallel::Unsafe
5163                        }
5164                        _ => return Ok(()),
5165                    };
5166                    self.pos += 2;
5167                    attrs.parallel = level;
5168                }
5169                "cost" | "rows" => {
5170                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5171                        return Ok(());
5172                    };
5173                    self.pos += 2;
5174                    if word == "cost" {
5175                        attrs.cost = Some(n);
5176                    } else {
5177                        attrs.rows = Some(n);
5178                    }
5179                }
5180                _ => return Ok(()),
5181            }
5182        }
5183    }
5184
5185    /// The numeric literal at `idx`, if there is one.
5186    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5187        match self.tokens.get(idx)? {
5188            Token::Integer(n) => Some(*n as f64),
5189            Token::Float(f) => Some(*f),
5190            Token::Numeric(t) => t.parse::<f64>().ok(),
5191            _ => None,
5192        }
5193    }
5194
5195    /// Closing `)`-terminated argument list. v7.12.4 commonly
5196    /// sees the empty `()`; typed args round-trip but the
5197    /// executor (yet) doesn't invoke them.
5198    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5199    /// it away, which is what PG does with one on a function parameter.
5200    fn skip_type_modifier(&mut self) {
5201        if !matches!(self.peek(), Token::LParen) {
5202            return;
5203        }
5204        // Only a numeric modifier — anything else is not one, and eating
5205        // it would swallow real grammar.
5206        let mut i = self.pos + 1;
5207        let mut seen_number = false;
5208        loop {
5209            match self.tokens.get(i) {
5210                Some(Token::Integer(_)) => seen_number = true,
5211                Some(Token::Comma) => {}
5212                Some(Token::RParen) => break,
5213                _ => return,
5214            }
5215            i += 1;
5216        }
5217        if !seen_number {
5218            return;
5219        }
5220        while self.pos <= i {
5221            self.advance();
5222        }
5223    }
5224
5225    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5226        let mut args: Vec<FunctionArg> = Vec::new();
5227        if matches!(self.peek(), Token::RParen) {
5228            self.advance();
5229            return Ok(args);
5230        }
5231        loop {
5232            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5233            // a reserved token; OUT / INOUT are bare idents.
5234            let mode = if matches!(self.peek(), Token::In) {
5235                self.advance();
5236                FunctionArgMode::In
5237            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5238            {
5239                self.advance();
5240                FunctionArgMode::Out
5241            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5242            {
5243                self.advance();
5244                FunctionArgMode::InOut
5245            } else {
5246                FunctionArgMode::In
5247            };
5248            // Optional name. The next token is either a name
5249            // (followed by a type ident) or the type itself.
5250            // Disambiguate by peeking ahead: if the token after
5251            // the next ident is also an ident, we treat the
5252            // first as the name.
5253            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5254            // the comma or paren, then decide. Reading at most two of
5255            // them could not spell `x double precision` at all, and
5256            // silently mis-read the bare `double precision` as a
5257            // parameter named "double" — which is what made the same
5258            // signature key two different ways.
5259            let (name, ty_token) = {
5260                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5261                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5262                    words.push(self.expect_ident_like()?);
5263                }
5264                // v7.39 (round 344) — a length / precision modifier on the
5265                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5266                // accepts it and DROPS it — `pg_get_function_arguments`
5267                // reports plain `character varying` / `numeric`, measured on
5268                // 18.4 — but SPG raised `syntax error at or near "("`,
5269                // because the modifier's parens were never consumed.
5270                self.skip_type_modifier();
5271                let whole = words.join(" ");
5272                if words.len() >= 2 && !is_multiword_type_phrase(&whole) {
5273                    (Some(words[0].clone()), words[1..].join(" "))
5274                } else {
5275                    (None, whole)
5276                }
5277            };
5278            // Type — try to map to ColumnTypeName, else Raw.
5279            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5280                Some(t) => FunctionArgType::Typed(t),
5281                None => FunctionArgType::Raw(ty_token),
5282            };
5283            args.push(FunctionArg { mode, name, ty });
5284            match self.peek() {
5285                Token::Comma => {
5286                    self.advance();
5287                    continue;
5288                }
5289                Token::RParen => {
5290                    self.advance();
5291                    return Ok(args);
5292                }
5293                other => {
5294                    return Err(self.err(alloc::format!(
5295                        "expected , or ) in function arg list, got {other:?}"
5296                    )));
5297                }
5298            }
5299        }
5300    }
5301
5302    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5303        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5304        // function whose row shape is named inline.
5305        if matches!(self.peek(), Token::Table)
5306            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5307        {
5308            self.advance(); // TABLE
5309            self.advance(); // (
5310            let mut cols: Vec<String> = Vec::new();
5311            loop {
5312                let cname = self.expect_ident_like()?;
5313                let mut ty: Vec<String> = Vec::new();
5314                loop {
5315                    match self.peek() {
5316                        Token::Comma | Token::RParen | Token::Eof => break,
5317                        _ => {}
5318                    }
5319                    match self.advance() {
5320                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5321                        other => {
5322                            if let Some(w) = unreserved_keyword_text(&other) {
5323                                ty.push(w);
5324                            }
5325                        }
5326                    }
5327                }
5328                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5329                if matches!(self.peek(), Token::Comma) {
5330                    self.advance();
5331                } else {
5332                    break;
5333                }
5334            }
5335            if matches!(self.peek(), Token::RParen) {
5336                self.advance();
5337            }
5338            return Ok(FunctionReturn::Other(alloc::format!(
5339                "TABLE({})",
5340                cols.join(", ")
5341            )));
5342        }
5343        let ident = self.expect_ident_like()?;
5344        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5345        if ident.eq_ignore_ascii_case("setof") {
5346            let inner = self.expect_ident_like()?;
5347            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5348            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5349        }
5350        if ident.eq_ignore_ascii_case("trigger") {
5351            return Ok(FunctionReturn::Trigger);
5352        }
5353        if ident.eq_ignore_ascii_case("void") {
5354            return Ok(FunctionReturn::Void);
5355        }
5356        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5357        // RETURN position did not, so the `[` was a syntax error and the
5358        // whole migration stopped. sentori worked around it by returning
5359        // zero-padded text.
5360        let suffix = self.consume_array_suffix();
5361        if !suffix.is_empty() {
5362            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5363        }
5364        match map_type_ident_to_column_type_name(&ident) {
5365            Some(t) => Ok(FunctionReturn::Type(t)),
5366            None => Ok(FunctionReturn::Other(ident)),
5367        }
5368    }
5369
5370    /// Consume any `[]` / `[N]` array markers after a type name and give
5371    /// back their text. Empty when there are none.
5372    fn consume_array_suffix(&mut self) -> String {
5373        let mut out = String::new();
5374        while matches!(self.peek(), Token::LBracket) {
5375            self.advance();
5376            // `[N]` is accepted and, as in PG, the length is not enforced.
5377            if let Token::Integer(n) = self.peek().clone() {
5378                self.advance();
5379                out.push_str(&alloc::format!("[{n}]"));
5380            } else {
5381                out.push_str("[]");
5382            }
5383            if matches!(self.peek(), Token::RBracket) {
5384                self.advance();
5385            }
5386        }
5387        out
5388    }
5389
5390    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5391        match self.peek() {
5392            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5393                self.advance();
5394                let lang = self.expect_ident_like()?;
5395                Ok(Some(lang.to_ascii_lowercase()))
5396            }
5397            _ => Ok(None),
5398        }
5399    }
5400
5401    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5402    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5403    /// (expr)]*`. The `DOMAIN` keyword has already been
5404    /// consumed. PG allows the trailing constraints in any
5405    /// order; we approximate with a small loop.
5406    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5407        let name = self.expect_ident_like()?;
5408        // Optional `AS`.
5409        if matches!(self.peek(), Token::As) {
5410            self.advance();
5411        }
5412        // v7.39 (round 259) — keep the raw type NAME when the base is not
5413        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5414        // parent domain.
5415        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5416            self.parse_type_with_implied_flags()?;
5417        let mut default: Option<Expr> = None;
5418        let mut not_null = false;
5419        let mut checks: Vec<Expr> = Vec::new();
5420        loop {
5421            match self.peek() {
5422                Token::Default => {
5423                    if default.is_some() {
5424                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5425                    }
5426                    self.advance();
5427                    default = Some(self.parse_expr(0)?);
5428                }
5429                Token::Not => {
5430                    self.advance();
5431                    if !matches!(self.peek(), Token::Null) {
5432                        return Err(self.err(alloc::format!(
5433                            "expected NULL after NOT in DOMAIN, got {:?}",
5434                            self.peek()
5435                        )));
5436                    }
5437                    self.advance();
5438                    not_null = true;
5439                }
5440                Token::Null => {
5441                    self.advance();
5442                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5443                    // is the default-nullable marker (PG accepts it),
5444                    // but AFTER a NOT NULL it is a conflict PG refuses
5445                    // (`conflicting NULL/NOT NULL constraints`,
5446                    // PG18-measured); the old arm no-opped both ways.
5447                    if not_null {
5448                        return Err(self.err(alloc::string::String::from(
5449                            "conflicting NULL/NOT NULL constraints",
5450                        )));
5451                    }
5452                }
5453                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5454                    self.advance();
5455                    if !matches!(self.peek(), Token::LParen) {
5456                        return Err(self.err(alloc::format!(
5457                            "expected '(' after CHECK in DOMAIN, got {:?}",
5458                            self.peek()
5459                        )));
5460                    }
5461                    self.advance();
5462                    let expr = self.parse_expr(0)?;
5463                    if !matches!(self.peek(), Token::RParen) {
5464                        return Err(self.err(alloc::format!(
5465                            "expected ')' after CHECK expr, got {:?}",
5466                            self.peek()
5467                        )));
5468                    }
5469                    self.advance();
5470                    checks.push(expr);
5471                }
5472                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5473                // prefix on the constraint; we drop the name and
5474                // recurse into the constraint parsing.
5475                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5476                    self.advance();
5477                    let _ = self.expect_ident_like()?;
5478                }
5479                _ => break,
5480            }
5481        }
5482        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5483            name,
5484            base_type,
5485            base_domain: base_user_ref,
5486            default,
5487            not_null,
5488            checks,
5489        }))
5490    }
5491
5492    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5493    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5494    /// consumed.
5495    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5496        let name = self.expect_ident_like()?;
5497        // Required `AS`.
5498        if !matches!(self.peek(), Token::As) {
5499            return Err(self.err(alloc::format!(
5500                "expected AS after CREATE TYPE {name:?}, got {:?}",
5501                self.peek()
5502            )));
5503        }
5504        self.advance();
5505        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5506        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5507        // on the next token: `(` = composite, ident `ENUM` = enum.
5508        if matches!(self.peek(), Token::LParen) {
5509            self.advance();
5510            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5511            let mut field_user_types: Vec<Option<String>> = Vec::new();
5512            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5513            // is legal PG (an attribute-less composite; measured — the old
5514            // e2e note claimed PG requires at least one attribute).
5515            if matches!(self.peek(), Token::RParen) {
5516                self.advance();
5517                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5518                    name,
5519                    kind: crate::ast::TypeKind::Composite {
5520                        fields,
5521                        field_user_types,
5522                    },
5523                }));
5524            }
5525            loop {
5526                let field_name = self.expect_ident_like()?;
5527                // v7.39 (round 264) — keep the raw type name when it is not
5528                // a builtin: that is how a NESTED composite field records
5529                // which composite it holds.
5530                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5531                    self.parse_type_with_implied_flags()?;
5532                fields.push((field_name, field_type));
5533                field_user_types.push(field_user_ref);
5534                if matches!(self.peek(), Token::Comma) {
5535                    self.advance();
5536                    continue;
5537                }
5538                if matches!(self.peek(), Token::RParen) {
5539                    self.advance();
5540                    break;
5541                }
5542                return Err(self.err(alloc::format!(
5543                    "expected , or ) in composite field list, got {:?}",
5544                    self.peek()
5545                )));
5546            }
5547            if fields.is_empty() {
5548                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5549            }
5550            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5551                name,
5552                kind: crate::ast::TypeKind::Composite {
5553                    fields,
5554                    field_user_types,
5555                },
5556            }));
5557        }
5558        // Required `ENUM` ident.
5559        let kind_ident = match self.peek().clone() {
5560            Token::Ident(s) | Token::QuotedIdent(s) => s,
5561            other => {
5562                return Err(self.err(alloc::format!(
5563                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5564                )));
5565            }
5566        };
5567        if !kind_ident.eq_ignore_ascii_case("enum") {
5568            return Err(self.err(alloc::format!(
5569                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5570            )));
5571        }
5572        self.advance();
5573        if !matches!(self.peek(), Token::LParen) {
5574            return Err(self.err(alloc::format!(
5575                "expected '(' after ENUM, got {:?}",
5576                self.peek()
5577            )));
5578        }
5579        self.advance();
5580        let mut labels: Vec<String> = Vec::new();
5581        loop {
5582            match self.peek().clone() {
5583                Token::String(s) => {
5584                    self.advance();
5585                    labels.push(s);
5586                }
5587                other => {
5588                    return Err(
5589                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5590                    );
5591                }
5592            }
5593            if matches!(self.peek(), Token::Comma) {
5594                self.advance();
5595                continue;
5596            }
5597            if matches!(self.peek(), Token::RParen) {
5598                self.advance();
5599                break;
5600            }
5601            return Err(self.err(alloc::format!(
5602                "expected , or ) in ENUM label list, got {:?}",
5603                self.peek()
5604            )));
5605        }
5606        if labels.is_empty() {
5607            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5608        }
5609        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5610            name,
5611            kind: crate::ast::TypeKind::Enum { labels },
5612        }))
5613    }
5614
5615    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5616    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5617    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5618    /// consumed.
5619    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5620        let if_not_exists = self.parse_if_not_exists();
5621        let name = self.expect_ident_like()?;
5622        let mut columns: Vec<String> = Vec::new();
5623        if matches!(self.peek(), Token::LParen) {
5624            self.advance();
5625            loop {
5626                let c = self.expect_ident_like()?;
5627                columns.push(c);
5628                if matches!(self.peek(), Token::Comma) {
5629                    self.advance();
5630                    continue;
5631                }
5632                if matches!(self.peek(), Token::RParen) {
5633                    self.advance();
5634                    break;
5635                }
5636                return Err(self.err(alloc::format!(
5637                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5638                    self.peek()
5639                )));
5640            }
5641        }
5642        if !matches!(self.peek(), Token::As) {
5643            return Err(self.err(alloc::format!(
5644                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5645                self.peek()
5646            )));
5647        }
5648        self.advance();
5649        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5650        // CTEs only; the engine rejects data-modifying ones with PG's
5651        // message). A trailing `WITH [NO] DATA` can't START the body,
5652        // so WITH here heads the query.
5653        let body = if self.peek_is_with_kw() {
5654            self.advance();
5655            self.parse_nested_with_select()?
5656        } else {
5657            let body_stmt = self.parse_select_stmt()?;
5658            let Statement::Select(body) = body_stmt else {
5659                return Err(self.err(alloc::format!(
5660                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5661                )));
5662            };
5663            body
5664        };
5665        // Optional trailing `WITH [NO] DATA`.
5666        let with_data = self.parse_optional_with_data(true)?;
5667        Ok(Statement::CreateMaterializedView(
5668            crate::ast::CreateMaterializedViewStatement {
5669                temporary: false,
5670                name,
5671                if_not_exists,
5672                columns,
5673                body,
5674                with_data,
5675                as_plain_table: false,
5676            },
5677        ))
5678    }
5679
5680    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5681    /// `default_when_absent` is what to return if the tail is
5682    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5683    /// WITH DATA).
5684    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5685        let save = self.pos;
5686        // `WITH` is an Ident (not reserved in the lexer).
5687        let is_with = match self.peek() {
5688            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5689            _ => false,
5690        };
5691        if !is_with {
5692            return Ok(default_when_absent);
5693        }
5694        self.advance();
5695        // Optional `NO`.
5696        let mut with_data = true;
5697        let is_no = match self.peek() {
5698            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5699            _ => false,
5700        };
5701        if is_no {
5702            self.advance();
5703            with_data = false;
5704        }
5705        // Required `DATA` ident.
5706        let is_data = match self.peek() {
5707            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5708            _ => false,
5709        };
5710        if is_data {
5711            self.advance();
5712            Ok(with_data)
5713        } else {
5714            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5715            // parser can interpret it.
5716            self.pos = save;
5717            Ok(default_when_absent)
5718        }
5719    }
5720
5721    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5722    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5723    /// All keyword prefixes have already been consumed; the flags
5724    /// say which were present.
5725    fn parse_create_view_after_keyword(
5726        &mut self,
5727        or_replace: bool,
5728        _materialized_unused: bool,
5729        temporary: bool,
5730    ) -> Result<Statement, ParseError> {
5731        let if_not_exists = self.parse_if_not_exists();
5732        let name = self.expect_ident_like()?;
5733        // Optional `(col, col, …)` rename list.
5734        let mut columns: Vec<String> = Vec::new();
5735        if matches!(self.peek(), Token::LParen) {
5736            self.advance();
5737            loop {
5738                let c = self.expect_ident_like()?;
5739                columns.push(c);
5740                if matches!(self.peek(), Token::Comma) {
5741                    self.advance();
5742                    continue;
5743                }
5744                if matches!(self.peek(), Token::RParen) {
5745                    self.advance();
5746                    break;
5747                }
5748                return Err(self.err(alloc::format!(
5749                    "expected , or ) in VIEW column list, got {:?}",
5750                    self.peek()
5751                )));
5752            }
5753        }
5754        // Required `AS`.
5755        if !matches!(self.peek(), Token::As) {
5756            return Err(self.err(alloc::format!(
5757                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5758                self.peek()
5759            )));
5760        }
5761        self.advance();
5762        // Body: a regular SELECT statement. v7.39 (round 151) — a
5763        // WITH-headed body is legal too (read-only CTEs only; the
5764        // engine rejects data-modifying ones with PG's message).
5765        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5766        // with the check-option clause, so WITH here heads the query.
5767        let body = if self.peek_is_with_kw() {
5768            self.advance();
5769            self.parse_nested_with_select()?
5770        } else {
5771            let body_stmt = self.parse_select_stmt()?;
5772            let Statement::Select(body) = body_stmt else {
5773                return Err(self.err(alloc::format!(
5774                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5775                )));
5776            };
5777            body
5778        };
5779        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5780        // The SELECT parser stops before a trailing WITH, so it lands here.
5781        let check_option = if matches!(self.peek(),
5782            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5783        {
5784            self.advance(); // WITH
5785            let opt = match self.peek() {
5786                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5787                    self.advance();
5788                    crate::ast::ViewCheckOption::Local
5789                }
5790                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5791                    self.advance();
5792                    crate::ast::ViewCheckOption::Cascaded
5793                }
5794                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5795                _ => crate::ast::ViewCheckOption::Cascaded,
5796            };
5797            if !matches!(self.peek(),
5798                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5799            {
5800                return Err(self.err(alloc::format!(
5801                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5802                    self.peek()
5803                )));
5804            }
5805            self.advance(); // CHECK
5806            if !matches!(self.peek(),
5807                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5808            {
5809                return Err(self.err(alloc::format!(
5810                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5811                    self.peek()
5812                )));
5813            }
5814            self.advance(); // OPTION
5815            Some(opt)
5816        } else {
5817            None
5818        };
5819        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5820            name,
5821            or_replace,
5822            if_not_exists,
5823            temporary,
5824            columns,
5825            body,
5826            check_option,
5827        }))
5828    }
5829
5830    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5831    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5832    /// consumed; `temporary` carries whether TEMPORARY was seen.
5833    fn parse_create_sequence_after_keyword(
5834        &mut self,
5835        temporary: bool,
5836    ) -> Result<Statement, ParseError> {
5837        let if_not_exists = self.parse_if_not_exists();
5838        let name = self.expect_ident_like()?;
5839        // Optional `AS data_type`.
5840        let data_type = if matches!(self.peek(), Token::As) {
5841            self.advance();
5842            Some(self.parse_sequence_data_type()?)
5843        } else {
5844            None
5845        };
5846        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5847        Ok(Statement::CreateSequence(
5848            crate::ast::CreateSequenceStatement {
5849                name,
5850                if_not_exists,
5851                temporary,
5852                data_type,
5853                options,
5854            },
5855        ))
5856    }
5857
5858    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5859    /// already been consumed; this is reached after `SEQUENCE`.
5860    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5861    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5862        use crate::ast::AlterDomainAction as A;
5863        let name = self.expect_ident_like()?;
5864        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5865        let kw = match self.peek() {
5866            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5867            Token::Drop => alloc::string::String::from("drop"),
5868            Token::Default => alloc::string::String::from("default"),
5869            other => {
5870                return Err(self.err(alloc::format!(
5871                    "expected an ALTER DOMAIN action, got {other:?}"
5872                )));
5873            }
5874        };
5875        let action = match kw.as_str() {
5876            "add" => {
5877                self.advance();
5878                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5879                {
5880                    self.advance();
5881                    Some(self.expect_ident_like()?)
5882                } else {
5883                    None
5884                };
5885                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5886                    return Err(self.err(alloc::format!(
5887                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5888                        self.peek()
5889                    )));
5890                }
5891                self.advance();
5892                if !matches!(self.peek(), Token::LParen) {
5893                    return Err(self.err("expected '(' after CHECK".into()));
5894                }
5895                self.advance();
5896                let check = self.parse_expr(0)?;
5897                if !matches!(self.peek(), Token::RParen) {
5898                    return Err(self.err("expected ')' after CHECK expression".into()));
5899                }
5900                self.advance();
5901                A::AddConstraint { name: cname, check }
5902            }
5903            "drop" => {
5904                self.advance();
5905                match self.peek() {
5906                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5907                        self.advance();
5908                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5909                        {
5910                            self.advance();
5911                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5912                            {
5913                                return Err(self.err("expected EXISTS after IF".into()));
5914                            }
5915                            self.advance();
5916                            true
5917                        } else {
5918                            false
5919                        };
5920                        let cn = self.expect_ident_like()?;
5921                        A::DropConstraint {
5922                            name: cn,
5923                            if_exists,
5924                        }
5925                    }
5926                    Token::Default => {
5927                        self.advance();
5928                        A::DropDefault
5929                    }
5930                    Token::Not => {
5931                        self.advance();
5932                        if !matches!(self.peek(), Token::Null) {
5933                            return Err(self.err("expected NULL after NOT".into()));
5934                        }
5935                        self.advance();
5936                        A::DropNotNull
5937                    }
5938                    other => {
5939                        return Err(self.err(alloc::format!(
5940                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5941                        )));
5942                    }
5943                }
5944            }
5945            "set" => {
5946                self.advance();
5947                match self.peek() {
5948                    Token::Default => {
5949                        self.advance();
5950                        A::SetDefault(self.parse_expr(0)?)
5951                    }
5952                    Token::Not => {
5953                        self.advance();
5954                        if !matches!(self.peek(), Token::Null) {
5955                            return Err(self.err("expected NULL after NOT".into()));
5956                        }
5957                        self.advance();
5958                        A::SetNotNull
5959                    }
5960                    other => {
5961                        return Err(self.err(alloc::format!(
5962                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5963                        )));
5964                    }
5965                }
5966            }
5967            "rename" => {
5968                self.advance();
5969                if !matches!(self.peek(), Token::To) {
5970                    return Err(self.err("expected TO after RENAME".into()));
5971                }
5972                self.advance();
5973                A::RenameTo(self.expect_ident_like()?)
5974            }
5975            other => {
5976                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5977            }
5978        };
5979        Ok(Statement::AlterDomain { name, action })
5980    }
5981
5982    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5983        let if_exists = self.parse_if_exists();
5984        let name = self.expect_ident_like()?;
5985        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5986        // the option list (PG allows only one or the other).
5987        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
5988            self.advance();
5989            if matches!(self.peek(), Token::To) {
5990                self.advance();
5991            } else {
5992                self.expect_keyword_ident("to")?;
5993            }
5994            let new = self.expect_ident_like()?;
5995            return Ok(Statement::AlterSequence(
5996                crate::ast::AlterSequenceStatement {
5997                    name,
5998                    if_exists,
5999                    options: crate::ast::SequenceOptions::default(),
6000                    rename_to: Some(new),
6001                },
6002            ));
6003        }
6004        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6005        Ok(Statement::AlterSequence(
6006            crate::ast::AlterSequenceStatement {
6007                name,
6008                if_exists,
6009                options,
6010                rename_to: None,
6011            },
6012        ))
6013    }
6014
6015    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6016        let kw = self.expect_ident_like()?;
6017        match kw.to_ascii_lowercase().as_str() {
6018            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6019            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6020            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6021            other => Err(self.err(alloc::format!(
6022                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6023            ))),
6024        }
6025    }
6026
6027    fn parse_sequence_options(
6028        &mut self,
6029        allow_restart: bool,
6030    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6031        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6032        let mut opts = SequenceOptions::default();
6033        #[allow(clippy::while_let_loop)]
6034        loop {
6035            // Match an ident; stop at any non-ident token (sentinel,
6036            // semicolon, end of statement).
6037            let kw_lc = match self.peek() {
6038                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6039                _ => break,
6040            };
6041            match kw_lc.as_str() {
6042                "increment" => {
6043                    self.advance();
6044                    // Optional BY.
6045                    if self.peek_is_by() {
6046                        self.advance();
6047                    }
6048                    opts.increment = Some(self.expect_signed_int()?);
6049                }
6050                "minvalue" => {
6051                    self.advance();
6052                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6053                }
6054                "maxvalue" => {
6055                    self.advance();
6056                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6057                }
6058                "no" => {
6059                    self.advance();
6060                    let what = self.expect_ident_like()?;
6061                    match what.to_ascii_lowercase().as_str() {
6062                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6063                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6064                        "cycle" => opts.cycle = Some(false),
6065                        other => {
6066                            return Err(self.err(alloc::format!(
6067                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6068                            )));
6069                        }
6070                    }
6071                }
6072                "start" => {
6073                    self.advance();
6074                    // Optional WITH.
6075                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6076                        if s.eq_ignore_ascii_case("with"))
6077                    {
6078                        self.advance();
6079                    }
6080                    opts.start = Some(self.expect_signed_int()?);
6081                }
6082                "restart" if allow_restart => {
6083                    self.advance();
6084                    // Optional WITH n; bare RESTART means restart at START.
6085                    let mut with_val: Option<i64> = None;
6086                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6087                        if s.eq_ignore_ascii_case("with"))
6088                    {
6089                        self.advance();
6090                        with_val = Some(self.expect_signed_int()?);
6091                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6092                        with_val = Some(self.expect_signed_int()?);
6093                    }
6094                    opts.restart = Some(with_val);
6095                }
6096                "cache" => {
6097                    self.advance();
6098                    opts.cache = Some(self.expect_signed_int()?);
6099                }
6100                "cycle" => {
6101                    self.advance();
6102                    opts.cycle = Some(true);
6103                }
6104                "owned" => {
6105                    self.advance();
6106                    match self.peek() {
6107                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6108                            self.advance();
6109                        }
6110                        other => {
6111                            return Err(
6112                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6113                            );
6114                        }
6115                    }
6116                    // OWNED BY {NONE | tab.col}. Read just one ident
6117                    // (NOT expect_ident_like which would auto-strip
6118                    // a schema prefix and consume the `.col` we need).
6119                    let first = match self.advance() {
6120                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6121                        other => {
6122                            return Err(self.err(alloc::format!(
6123                                "expected identifier or NONE after OWNED BY, got {other:?}"
6124                            )));
6125                        }
6126                    };
6127                    if first.eq_ignore_ascii_case("none") {
6128                        opts.owned_by = Some(SequenceOwnedBy::None);
6129                    } else if matches!(self.peek(), Token::Dot) {
6130                        self.advance();
6131                        let second = match self.advance() {
6132                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6133                            other => {
6134                                return Err(self.err(alloc::format!(
6135                                    "expected column name after OWNED BY {first}., got {other:?}"
6136                                )));
6137                            }
6138                        };
6139                        // v7.17 dump-compat fix — pg_dump emits
6140                        // OWNED BY clauses as
6141                        // `schema.table.column` (three segments).
6142                        // If a third `.<ident>` follows, treat the
6143                        // first ident as schema (drop it; SPG is
6144                        // single-schema) and the middle / last
6145                        // pair as table.column. Otherwise it's
6146                        // the two-segment form table.column.
6147                        if matches!(self.peek(), Token::Dot) {
6148                            self.advance();
6149                            let third = match self.advance() {
6150                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6151                                other => {
6152                                    return Err(self.err(alloc::format!(
6153                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6154                                    )));
6155                                }
6156                            };
6157                            let _ = first; // schema prefix discarded
6158                            opts.owned_by = Some(SequenceOwnedBy::Column {
6159                                table: second,
6160                                column: third,
6161                            });
6162                        } else {
6163                            opts.owned_by = Some(SequenceOwnedBy::Column {
6164                                table: first,
6165                                column: second,
6166                            });
6167                        }
6168                    } else {
6169                        return Err(self.err(alloc::format!(
6170                            "expected table.column or NONE after OWNED BY, got {first:?}"
6171                        )));
6172                    }
6173                }
6174                _ => break,
6175            }
6176        }
6177        Ok(opts)
6178    }
6179
6180    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6181        let neg = if matches!(self.peek(), Token::Minus) {
6182            self.advance();
6183            true
6184        } else {
6185            false
6186        };
6187        match self.peek() {
6188            Token::Integer(n) => {
6189                let v = *n;
6190                self.advance();
6191                Ok(if neg { -v } else { v })
6192            }
6193            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6194        }
6195    }
6196
6197    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6198    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6199    /// clause is fully accepted and discarded — SPG always runs
6200    /// constraint checks immediately (single-writer model). The
6201    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6202    /// in either order (per the SQL spec they're independent),
6203    /// though pg_dump always emits them in the canonical
6204    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6205    /// Stops at the first token that isn't part of the clause.
6206    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6207        self.consume_deferrable_clauses_timed().map(|_| ())
6208    }
6209
6210    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6211    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6212    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6213    /// NOT DEFERRABLE and a circular-FK migration could not load.
6214    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6215        let mut deferrable = false;
6216        let mut initially_deferred = false;
6217        loop {
6218            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6219            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6220                self.advance();
6221                deferrable = true;
6222                if self.consume_optional_initially_clause()? {
6223                    initially_deferred = true;
6224                }
6225                continue;
6226            }
6227            // `NOT DEFERRABLE` — already worked pre-3.1.
6228            if matches!(self.peek(), Token::Not) {
6229                let look = self.tokens.get(self.pos + 1);
6230                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6231                    self.advance(); // NOT
6232                    self.advance(); // DEFERRABLE
6233                    deferrable = false;
6234                    initially_deferred = false;
6235                    let _ = self.consume_optional_initially_clause()?;
6236                    continue;
6237                }
6238                break;
6239            }
6240            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6241            // accepts this without a leading [NOT] DEFERRABLE
6242            // (the timing keyword alone). pg_dump occasionally
6243            // emits it on FK constraints that inherit timing.
6244            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6245                if self.consume_optional_initially_clause()? {
6246                    initially_deferred = true;
6247                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6248                    deferrable = true;
6249                }
6250                continue;
6251            }
6252            break;
6253        }
6254        Ok((deferrable, initially_deferred))
6255    }
6256
6257    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6258    /// next token is `INITIALLY`, consume it plus the required
6259    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6260    /// Returns true when the timing seen was `DEFERRED`.
6261    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6262        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6263            return Ok(false);
6264        }
6265        self.advance(); // INITIALLY
6266        match self.advance() {
6267            Token::Ident(s)
6268                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6269            {
6270                Ok(s.eq_ignore_ascii_case("deferred"))
6271            }
6272            other => Err(self.err(alloc::format!(
6273                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6274            ))),
6275        }
6276    }
6277
6278    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6279    /// in its entirety so the parser returns Empty without
6280    /// touching the runtime. The CREATE+PROCEDURE keywords are
6281    /// already consumed; this swallows everything from the
6282    /// procedure name through the matching `END`, including
6283    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6284    /// (DELIMITER `//` makes the script splitter forward the
6285    /// whole block as one statement), `@var` session-variable
6286    /// references, and the trailing terminator.
6287    ///
6288    /// Tracks nesting depth so:
6289    ///   BEGIN
6290    ///     IF cond THEN
6291    ///       BEGIN ... END;
6292    ///     END IF;
6293    ///   END
6294    /// terminates at the outer END.
6295    fn consume_mysql_routine_body(&mut self) {
6296        // Outer skeleton: name, (...), optional clauses, BEGIN
6297        // <body> END [;]. Scan for the first BEGIN — anything
6298        // before it is signature decoration we don't care about.
6299        // Once inside BEGIN, count up on BEGIN, down on END.
6300        let mut depth: i32 = 0;
6301        let mut started = false;
6302        loop {
6303            match self.peek().clone() {
6304                Token::Begin => {
6305                    self.advance();
6306                    depth += 1;
6307                    started = true;
6308                }
6309                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6310                    self.advance();
6311                    if started {
6312                        depth -= 1;
6313                        if depth <= 0 {
6314                            // Optional trailing ident (`END IF`,
6315                            // `END LOOP`, `END WHILE`, `END CASE`,
6316                            // `END label_name`) — eat the next
6317                            // ident if present so we don't
6318                            // mistake `END IF;` for the outer
6319                            // close.
6320                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6321                                // If the next token is one of the
6322                                // PL/SQL block-closer keywords,
6323                                // the END belongs to an inner
6324                                // block; bump depth back up.
6325                                let is_inner_close = matches!(
6326                                    self.peek(),
6327                                    Token::Ident(s) | Token::QuotedIdent(s)
6328                                        if matches!(
6329                                            s.to_ascii_lowercase().as_str(),
6330                                            "if" | "loop" | "while" | "case" | "repeat"
6331                                        )
6332                                );
6333                                if is_inner_close {
6334                                    self.advance();
6335                                    depth += 1;
6336                                    continue;
6337                                }
6338                            }
6339                            // Eat optional trailing `;`.
6340                            if matches!(self.peek(), Token::Semicolon) {
6341                                self.advance();
6342                            }
6343                            return;
6344                        }
6345                    }
6346                }
6347                Token::Eof => return,
6348                _ => {
6349                    self.advance();
6350                }
6351            }
6352        }
6353    }
6354
6355    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6356    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6357    ///
6358    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6359    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6360    ///   ident, or `ident @ ident-or-quoted-string` host form)
6361    /// * `SQL SECURITY {DEFINER|INVOKER}`
6362    ///
6363    /// Each clause may appear at most once but in any order.
6364    /// The hints are pure planner / permission metadata that
6365    /// SPG's view-rewrite engine handles uniformly; we accept
6366    /// and discard. Returns `Ok(())` once a non-clause token is
6367    /// peeked (the caller then checks for the `VIEW` keyword).
6368    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6369        loop {
6370            match self.peek().clone() {
6371                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6372                    self.advance(); // ALGORITHM
6373                    // Optional `=`. MySQL spec requires it but be
6374                    // generous.
6375                    if matches!(self.peek(), Token::Eq) {
6376                        self.advance();
6377                    }
6378                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6379                    // bare ident; unknown values still parse so
6380                    // future MySQL versions don't break.
6381                    if matches!(
6382                        self.peek(),
6383                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6384                    ) {
6385                        self.advance();
6386                    }
6387                }
6388                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6389                    self.advance(); // DEFINER
6390                    if matches!(self.peek(), Token::Eq) {
6391                        self.advance();
6392                    }
6393                    // User: quoted string, ident, OR ident @ host
6394                    // (host may itself be quoted or bare).
6395                    match self.peek().clone() {
6396                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6397                            self.advance();
6398                            // Optional `@host`.
6399                            if matches!(self.peek(), Token::At) {
6400                                self.advance();
6401                                if matches!(
6402                                    self.peek(),
6403                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6404                                ) {
6405                                    self.advance();
6406                                }
6407                            }
6408                        }
6409                        _ => {}
6410                    }
6411                }
6412                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6413                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6414                    // when followed by SECURITY — the dispatcher must
6415                    // not consume a bare `SQL` token (it's not a
6416                    // legal CREATE prefix on its own).
6417                    let save = self.pos;
6418                    self.advance(); // SQL
6419                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6420                        if s2.eq_ignore_ascii_case("security"))
6421                    {
6422                        self.advance(); // SECURITY
6423                        // DEFINER / INVOKER trailing ident.
6424                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6425                            self.advance();
6426                        }
6427                    } else {
6428                        // Not a SQL SECURITY clause — roll back and
6429                        // bail; the caller will error out cleanly.
6430                        self.pos = save;
6431                        return Ok(());
6432                    }
6433                }
6434                _ => return Ok(()),
6435            }
6436        }
6437    }
6438
6439    fn parse_if_not_exists(&mut self) -> bool {
6440        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6441        {
6442            let save = self.pos;
6443            self.advance();
6444            if matches!(self.peek(), Token::Not) {
6445                self.advance();
6446                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6447                {
6448                    self.advance();
6449                    return true;
6450                }
6451            }
6452            self.pos = save;
6453        }
6454        false
6455    }
6456
6457    fn parse_if_exists(&mut self) -> bool {
6458        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6459        {
6460            let save = self.pos;
6461            self.advance();
6462            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6463            {
6464                self.advance();
6465                return true;
6466            }
6467            self.pos = save;
6468        }
6469        false
6470    }
6471
6472    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6473    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6474    /// been consumed.
6475    fn parse_create_trigger_after_keyword(
6476        &mut self,
6477        or_replace: bool,
6478    ) -> Result<Statement, ParseError> {
6479        let name = self.expect_ident_like()?;
6480        let timing = {
6481            let ident = self.expect_ident_like()?;
6482            if ident.eq_ignore_ascii_case("before") {
6483                TriggerTiming::Before
6484            } else if ident.eq_ignore_ascii_case("after") {
6485                TriggerTiming::After
6486            } else if ident.eq_ignore_ascii_case("instead") {
6487                let next = self.expect_ident_like()?;
6488                if !next.eq_ignore_ascii_case("of") {
6489                    return Err(self.err(alloc::format!(
6490                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6491                    )));
6492                }
6493                TriggerTiming::InsteadOf
6494            } else {
6495                return Err(self.err(alloc::format!(
6496                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6497                )));
6498            }
6499        };
6500        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6501        // OR is a reserved keyword token (Token::Or), not an Ident.
6502        // v7.13.0 — after an UPDATE event we may optionally see
6503        // `OF col, col, …` (mailrs round-5 G7). Columns are
6504        // captured into `update_columns` once across the whole
6505        // events list; multiple `UPDATE OF` clauses are rejected.
6506        let mut events: Vec<TriggerEvent> = Vec::new();
6507        let mut update_columns: Vec<String> = Vec::new();
6508        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6509        events.push(first_ev);
6510        if !first_cols.is_empty() {
6511            update_columns = first_cols;
6512        }
6513        while matches!(self.peek(), Token::Or) {
6514            self.advance();
6515            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6516            events.push(ev);
6517            if !cols.is_empty() {
6518                if !update_columns.is_empty() {
6519                    return Err(
6520                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6521                    );
6522                }
6523                update_columns = cols;
6524            }
6525        }
6526        // ON <table>
6527        let tok = self.peek();
6528        let Token::On = tok else {
6529            return Err(self.err(alloc::format!(
6530                "expected ON after trigger events, got {tok:?}"
6531            )));
6532        };
6533        self.advance();
6534        let table = self.expect_ident_like()?;
6535        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6536        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6537        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6538        // the trigger as a plain AFTER trigger (correct for every non-deferred
6539        // use; deferral timing is not yet honoured).
6540        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6541            if s.eq_ignore_ascii_case("from"))
6542        {
6543            self.advance();
6544            let _reftable = self.expect_ident_like()?;
6545        }
6546        self.consume_optional_deferrable_clauses()?;
6547        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6548        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6549        // idents.
6550        if !matches!(self.peek(), Token::For) {
6551            return Err(self.err(alloc::format!(
6552                "expected FOR EACH ROW / STATEMENT, got {:?}",
6553                self.peek()
6554            )));
6555        }
6556        self.advance();
6557        let for_each = {
6558            let e = self.expect_ident_like()?;
6559            if !e.eq_ignore_ascii_case("each") {
6560                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6561            }
6562            let unit = self.expect_ident_like()?;
6563            if unit.eq_ignore_ascii_case("row") {
6564                TriggerForEach::Row
6565            } else if unit.eq_ignore_ascii_case("statement") {
6566                TriggerForEach::Statement
6567            } else {
6568                return Err(self.err(alloc::format!(
6569                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6570                )));
6571            }
6572        };
6573        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6574        let when_condition = if matches!(self.peek(),
6575            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6576        {
6577            self.advance();
6578            Some(self.parse_paren_expr("WHEN")?)
6579        } else {
6580            None
6581        };
6582        // EXECUTE FUNCTION/PROCEDURE name(...)
6583        let exec = self.expect_ident_like()?;
6584        if !exec.eq_ignore_ascii_case("execute") {
6585            return Err(self.err(alloc::format!(
6586                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6587            )));
6588        }
6589        let fn_or_proc = self.expect_ident_like()?;
6590        if !(fn_or_proc.eq_ignore_ascii_case("function")
6591            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6592        {
6593            return Err(self.err(alloc::format!(
6594                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6595            )));
6596        }
6597        let function = self.expect_ident_like()?;
6598        // Optional empty arg list `()`.
6599        if matches!(self.peek(), Token::LParen) {
6600            self.advance();
6601            if !matches!(self.peek(), Token::RParen) {
6602                return Err(self.err(alloc::format!(
6603                    "v7.12.4 trigger function calls take no args; got {:?}",
6604                    self.peek()
6605                )));
6606            }
6607            self.advance();
6608        }
6609        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6610            name,
6611            or_replace,
6612            timing,
6613            events,
6614            table,
6615            for_each,
6616            function,
6617            update_columns,
6618            when_condition,
6619        }))
6620    }
6621
6622    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6623    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6624    fn parse_create_rule_after_keyword(
6625        &mut self,
6626        or_replace: bool,
6627    ) -> Result<Statement, ParseError> {
6628        let name = self.expect_ident_like()?;
6629        if !matches!(self.peek(), Token::As) {
6630            return Err(self.err(alloc::format!(
6631                "expected AS in CREATE RULE, got {:?}",
6632                self.peek()
6633            )));
6634        }
6635        self.advance();
6636        if !matches!(self.peek(), Token::On) {
6637            return Err(self.err(alloc::format!(
6638                "expected ON in CREATE RULE, got {:?}",
6639                self.peek()
6640            )));
6641        }
6642        self.advance();
6643        let event = self.parse_rule_event()?;
6644        if !matches!(self.peek(), Token::To)
6645            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6646        {
6647            return Err(self.err(alloc::format!(
6648                "expected TO after rule event, got {:?}",
6649                self.peek()
6650            )));
6651        }
6652        self.advance();
6653        let table = self.expect_ident_like()?;
6654        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6655        let when_condition = if matches!(self.peek(), Token::Where) {
6656            self.advance();
6657            Some(self.parse_expr(0)?)
6658        } else {
6659            None
6660        };
6661        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6662        {
6663            return Err(self.err(alloc::format!(
6664                "expected DO in CREATE RULE, got {:?}",
6665                self.peek()
6666            )));
6667        }
6668        self.advance();
6669        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6670        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6671        {
6672            self.advance();
6673            true
6674        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6675            self.advance();
6676            false
6677        } else {
6678            false
6679        };
6680        // `NOTHING` | `( cmd; … )` | `cmd`.
6681        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6682        {
6683            self.advance();
6684            Vec::new()
6685        } else if matches!(self.peek(), Token::LParen) {
6686            self.advance();
6687            let mut cmds = Vec::new();
6688            loop {
6689                cmds.push(self.parse_one_statement()?);
6690                if matches!(self.peek(), Token::Semicolon) {
6691                    self.advance();
6692                    if matches!(self.peek(), Token::RParen) {
6693                        break;
6694                    }
6695                    continue;
6696                }
6697                break;
6698            }
6699            if !matches!(self.peek(), Token::RParen) {
6700                return Err(self.err(alloc::format!(
6701                    "expected ) closing the CREATE RULE command list, got {:?}",
6702                    self.peek()
6703                )));
6704            }
6705            self.advance();
6706            cmds
6707        } else {
6708            alloc::vec![self.parse_one_statement()?]
6709        };
6710        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6711            name,
6712            or_replace,
6713            event,
6714            table,
6715            instead,
6716            when_condition,
6717            commands,
6718        }))
6719    }
6720
6721    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6722    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6723        if matches!(self.peek(), Token::Insert) {
6724            self.advance();
6725            return Ok(alloc::string::String::from("INSERT"));
6726        }
6727        if matches!(self.peek(), Token::Select) {
6728            self.advance();
6729            return Ok(alloc::string::String::from("SELECT"));
6730        }
6731        match self.peek() {
6732            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6733                self.advance();
6734                Ok(alloc::string::String::from("UPDATE"))
6735            }
6736            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6737                self.advance();
6738                Ok(alloc::string::String::from("DELETE"))
6739            }
6740            other => Err(self.err(alloc::format!(
6741                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6742            ))),
6743        }
6744    }
6745
6746    /// v7.13.0 — parse one trigger event, then optionally consume
6747    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6748    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6749    fn parse_trigger_event_with_optional_of(
6750        &mut self,
6751    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6752        let ev = self.parse_trigger_event()?;
6753        if !matches!(ev, TriggerEvent::Update) {
6754            return Ok((ev, Vec::new()));
6755        }
6756        // `OF` is a bare ident.
6757        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6758            return Ok((ev, Vec::new()));
6759        }
6760        self.advance(); // OF
6761        let mut cols: Vec<String> = Vec::new();
6762        loop {
6763            cols.push(self.expect_ident_like()?);
6764            if matches!(self.peek(), Token::Comma) {
6765                self.advance();
6766                continue;
6767            }
6768            break;
6769        }
6770        if cols.is_empty() {
6771            return Err(
6772                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6773            );
6774        }
6775        Ok((ev, cols))
6776    }
6777
6778    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6779    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6780    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6781    /// inside the body.
6782    /// Called by [`parse_plpgsql_body`] after the body's tokens
6783    /// have been lexed into this temporary parser.
6784    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6785        // v7.12.6 — optional DECLARE prelude.
6786        let declarations = if matches!(
6787            self.peek(),
6788            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6789        ) {
6790            self.advance();
6791            self.parse_plpgsql_declare_block()?
6792        } else {
6793            Vec::new()
6794        };
6795        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6796        // `BEGIN` transaction-start, but we can reuse the
6797        // reserved Token::Begin since the body is a separate
6798        // lex/parse context).
6799        if !matches!(self.peek(), Token::Begin) {
6800            return Err(self.err(alloc::format!(
6801                "expected BEGIN at start of plpgsql block, got {:?}",
6802                self.peek()
6803            )));
6804        }
6805        self.advance();
6806        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6807        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6808        // body's last statement and the trailing END. When present
6809        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6810        // arms terminated by END.
6811        let exception_handlers = if matches!(
6812            self.peek(),
6813            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6814        ) {
6815            self.advance();
6816            self.parse_plpgsql_exception_handlers()?
6817        } else {
6818            Vec::new()
6819        };
6820        Ok(PlPgSqlBlock {
6821            declarations,
6822            statements,
6823            exception_handlers,
6824        })
6825    }
6826
6827    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6828    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6829    fn parse_plpgsql_exception_handlers(
6830        &mut self,
6831    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6832        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6833        loop {
6834            // Stop at END — the block-level trailing END LOOP / END;
6835            // is handled by the caller.
6836            if matches!(
6837                self.peek(),
6838                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6839            ) {
6840                return Ok(out);
6841            }
6842            // WHEN <cond> [OR <cond>]* THEN <body>
6843            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6844            {
6845                return Err(self.err(alloc::format!(
6846                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
6847                    self.peek()
6848                )));
6849            }
6850            self.advance();
6851            let mut conditions: Vec<String> = Vec::new();
6852            conditions.push(self.expect_ident_like()?);
6853            while matches!(self.peek(), Token::Or) {
6854                self.advance();
6855                conditions.push(self.expect_ident_like()?);
6856            }
6857            let then_kw = self.expect_ident_like()?;
6858            if !then_kw.eq_ignore_ascii_case("then") {
6859                return Err(self.err(alloc::format!(
6860                    "expected THEN after WHEN condition list, got {then_kw:?}"
6861                )));
6862            }
6863            let body = self.parse_plpgsql_stmt_list_until_end()?;
6864            out.push(crate::ast::ExceptionHandler { conditions, body });
6865        }
6866    }
6867
6868    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6869    /// prelude. Caller has already consumed `DECLARE`. We stop
6870    /// reading entries when we hit `BEGIN`.
6871    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6872        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6873        loop {
6874            if matches!(self.peek(), Token::Begin) {
6875                return Ok(out);
6876            }
6877            let name = self.expect_ident_like()?;
6878            // v7.37.20 (20.7) — type inference: if the next token is
6879            // `:=` or `=` (no explicit type), infer from the default
6880            // expression. Otherwise the ident that follows is the
6881            // declared type.
6882            //
6883            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6884            // (PG-standard). SPG parse-accepts and treats identically
6885            // to inference — the eventual runtime value determines
6886            // the local's type, which is faithful to how SPG handles
6887            // untyped locals today (see 20.7). Full compile-time
6888            // catalog lookup queues with v7.40 PL/pgSQL epic.
6889            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6890                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6891                // downstream declaration walker to type the local by
6892                // the runtime type of the default expression.
6893                FunctionArgType::Raw("_infer_".into())
6894            } else {
6895                let ty_token = self.expect_ident_like()?;
6896                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6897                // consume optional `.<ident>` qualifier + `%<KW>`
6898                // suffix. Both qualifier and suffix map to _infer_.
6899                if matches!(self.peek(), Token::Dot) {
6900                    self.advance();
6901                    let _ = self.expect_ident_like()?;
6902                }
6903                if matches!(self.peek(), Token::Percent) {
6904                    self.advance();
6905                    // Consume the trailing TYPE / ROWTYPE ident.
6906                    let _ = self.expect_ident_like()?;
6907                    FunctionArgType::Raw("_infer_".into())
6908                } else {
6909                    match map_type_ident_to_column_type_name(&ty_token) {
6910                        Some(t) => FunctionArgType::Typed(t),
6911                        None => FunctionArgType::Raw(ty_token),
6912                    }
6913                }
6914            };
6915            let default = match self.peek() {
6916                Token::ColonEq => {
6917                    self.advance();
6918                    Some(self.parse_expr(0)?)
6919                }
6920                Token::Eq => {
6921                    // PL/pgSQL also accepts `=` for the
6922                    // DECLARE default (PG treats them the same
6923                    // in this position).
6924                    self.advance();
6925                    Some(self.parse_expr(0)?)
6926                }
6927                _ => None,
6928            };
6929            // Mandatory `;` between declarations.
6930            if !matches!(self.peek(), Token::Semicolon) {
6931                return Err(self.err(alloc::format!(
6932                    "expected ; after DECLARE entry for {name:?}, got {:?}",
6933                    self.peek()
6934                )));
6935            }
6936            self.advance();
6937            out.push(PlPgSqlDeclare { name, ty, default });
6938        }
6939    }
6940
6941    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6942    /// the terminating `END;` (or `END IF;` etc — handled by the
6943    /// per-construct sub-parsers). Used by both the outer block
6944    /// and the IF/ELSE branch bodies.
6945    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6946        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6947        loop {
6948            // Allow trailing semicolons + END.
6949            while matches!(self.peek(), Token::Semicolon) {
6950                self.advance();
6951            }
6952            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6953            if matches!(
6954                self.peek(),
6955                Token::Ident(s) | Token::QuotedIdent(s)
6956                    if s.eq_ignore_ascii_case("end")
6957                        || s.eq_ignore_ascii_case("else")
6958                        || s.eq_ignore_ascii_case("elsif")
6959                        || s.eq_ignore_ascii_case("elseif")
6960                        || s.eq_ignore_ascii_case("exception")
6961                        || s.eq_ignore_ascii_case("when")
6962            ) {
6963                return Ok(statements);
6964            }
6965            // Otherwise: one statement, then expect `;` or
6966            // a block-terminator keyword.
6967            let stmt = self.parse_plpgsql_stmt()?;
6968            statements.push(stmt);
6969            match self.peek() {
6970                Token::Semicolon => {
6971                    self.advance();
6972                }
6973                Token::Ident(s) | Token::QuotedIdent(s)
6974                    if s.eq_ignore_ascii_case("end")
6975                        || s.eq_ignore_ascii_case("else")
6976                        || s.eq_ignore_ascii_case("elsif")
6977                        || s.eq_ignore_ascii_case("elseif")
6978                        || s.eq_ignore_ascii_case("exception")
6979                        || s.eq_ignore_ascii_case("when") =>
6980                {
6981                    // Final statement of the block without `;`.
6982                }
6983                other => {
6984                    return Err(self.err(alloc::format!(
6985                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6986                    )));
6987                }
6988            }
6989        }
6990    }
6991
6992    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
6993        // RETURN keyword?
6994        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
6995        {
6996            self.advance();
6997            return self.parse_plpgsql_return();
6998        }
6999        // v7.12.6 — IF block.
7000        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7001        {
7002            self.advance();
7003            return self.parse_plpgsql_if();
7004        }
7005        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7006        // Detected by peeking that token pos+3 is Ident("execute").
7007        if matches!(self.peek(), Token::For)
7008            && matches!(
7009                self.tokens.get(self.pos + 1),
7010                Some(Token::Ident(_) | Token::QuotedIdent(_))
7011            )
7012            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7013            && matches!(
7014                self.tokens.get(self.pos + 3),
7015                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7016            )
7017        {
7018            self.advance(); // FOR
7019            let var = self.expect_ident_like()?;
7020            self.advance(); // IN
7021            self.advance(); // EXECUTE
7022            // Prescan for LOOP at paren depth 0 so parse_expr stops
7023            // before the LOOP keyword (same trick as the bare-SELECT
7024            // ForQuery arm).
7025            let mut depth: i32 = 0;
7026            let mut loop_pos: Option<usize> = None;
7027            let mut scan = self.pos;
7028            while scan < self.tokens.len() {
7029                match self.tokens.get(scan) {
7030                    Some(Token::LParen) => depth += 1,
7031                    Some(Token::RParen) => depth -= 1,
7032                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7033                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7034                    {
7035                        loop_pos = Some(scan);
7036                        break;
7037                    }
7038                    _ => {}
7039                }
7040                scan += 1;
7041            }
7042            let loop_pos = loop_pos.ok_or_else(|| {
7043                self.err(alloc::format!(
7044                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7045                ))
7046            })?;
7047            let saved_loop = self.tokens[loop_pos].clone();
7048            self.tokens[loop_pos] = Token::Semicolon;
7049            let expr_result = self.parse_expr(0);
7050            self.tokens[loop_pos] = saved_loop;
7051            let sql_expr = expr_result?;
7052            let loop_kw = self.expect_ident_like()?;
7053            if !loop_kw.eq_ignore_ascii_case("loop") {
7054                return Err(self.err(alloc::format!(
7055                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7056                )));
7057            }
7058            let body = self.parse_plpgsql_stmt_list_until_end()?;
7059            let end_kw = self.expect_ident_like()?;
7060            if !end_kw.eq_ignore_ascii_case("end") {
7061                return Err(self.err(alloc::format!(
7062                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7063                )));
7064            }
7065            let loop_kw2 = self.expect_ident_like()?;
7066            if !loop_kw2.eq_ignore_ascii_case("loop") {
7067                return Err(self.err(alloc::format!(
7068                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7069                )));
7070            }
7071            return Ok(PlPgSqlStmt::ForExecute {
7072                var,
7073                sql_expr,
7074                body,
7075            });
7076        }
7077        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7078        //
7079        // Two syntactic forms:
7080        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7081        //   FOR var IN (SELECT ...) LOOP ...
7082        //
7083        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7084        // the trailing `LOOP` keyword as a table alias, we prescan
7085        // forward to find LOOP at paren depth 0, splice a fake
7086        // Semicolon at that position (so SELECT parses cleanly),
7087        // then re-splice LOOP back in.
7088        //
7089        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7090        // LOOP directly — no scan required.
7091        if matches!(self.peek(), Token::For)
7092            && matches!(
7093                self.tokens.get(self.pos + 1),
7094                Some(Token::Ident(_) | Token::QuotedIdent(_))
7095            )
7096            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7097            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7098                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7099        {
7100            self.advance(); // FOR
7101            let var = self.expect_ident_like()?;
7102            // IN
7103            self.advance();
7104            let query = if matches!(self.peek(), Token::LParen) {
7105                // Paren-wrapped SELECT.
7106                self.advance();
7107                let inner = self.parse_select_stmt()?;
7108                let Statement::Select(q) = inner else {
7109                    return Err(self.err(alloc::format!(
7110                        "expected SELECT inside (…), got {:?}",
7111                        self.peek()
7112                    )));
7113                };
7114                if !matches!(self.peek(), Token::RParen) {
7115                    return Err(self.err(alloc::format!(
7116                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7117                        self.peek()
7118                    )));
7119                }
7120                self.advance();
7121                q
7122            } else {
7123                // Bare SELECT: prescan to find the LOOP boundary.
7124                let mut depth: i32 = 0;
7125                let mut loop_pos: Option<usize> = None;
7126                let mut scan = self.pos;
7127                while scan < self.tokens.len() {
7128                    match self.tokens.get(scan) {
7129                        Some(Token::LParen) => depth += 1,
7130                        Some(Token::RParen) => depth -= 1,
7131                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7132                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7133                        {
7134                            loop_pos = Some(scan);
7135                            break;
7136                        }
7137                        _ => {}
7138                    }
7139                    scan += 1;
7140                }
7141                let loop_pos = loop_pos.ok_or_else(|| {
7142                    self.err(alloc::format!(
7143                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7144                    ))
7145                })?;
7146                // Swap the LOOP token with a synthetic Semicolon so
7147                // parse_select_stmt stops there, then restore afterward.
7148                let saved_loop = self.tokens[loop_pos].clone();
7149                self.tokens[loop_pos] = Token::Semicolon;
7150                let parse_result = self.parse_select_stmt();
7151                self.tokens[loop_pos] = saved_loop;
7152                let inner = parse_result?;
7153                let Statement::Select(q) = inner else {
7154                    return Err(self.err(alloc::format!(
7155                        "expected SELECT after FOR <var> IN, got {:?}",
7156                        self.peek()
7157                    )));
7158                };
7159                q
7160            };
7161            let loop_kw = self.expect_ident_like()?;
7162            if !loop_kw.eq_ignore_ascii_case("loop") {
7163                return Err(self.err(alloc::format!(
7164                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7165                )));
7166            }
7167            let body = self.parse_plpgsql_stmt_list_until_end()?;
7168            let end_kw = self.expect_ident_like()?;
7169            if !end_kw.eq_ignore_ascii_case("end") {
7170                return Err(self.err(alloc::format!(
7171                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7172                )));
7173            }
7174            let loop_kw2 = self.expect_ident_like()?;
7175            if !loop_kw2.eq_ignore_ascii_case("loop") {
7176                return Err(self.err(alloc::format!(
7177                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7178                )));
7179            }
7180            return Ok(PlPgSqlStmt::ForQuery {
7181                var,
7182                query: Box::new(query),
7183                body,
7184            });
7185        }
7186        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7187        // FOR is a reserved keyword token (Token::For).
7188        if matches!(self.peek(), Token::For)
7189            && matches!(
7190                self.tokens.get(self.pos + 1),
7191                Some(Token::Ident(_) | Token::QuotedIdent(_))
7192            )
7193            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7194        {
7195            self.advance(); // FOR
7196            let var = self.expect_ident_like()?;
7197            if !matches!(self.peek(), Token::In) {
7198                return Err(self.err(alloc::format!(
7199                    "expected IN after FOR <var>, got {:?}",
7200                    self.peek()
7201                )));
7202            }
7203            self.advance();
7204            let reverse = matches!(
7205                self.peek(),
7206                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7207            );
7208            if reverse {
7209                self.advance();
7210            }
7211            let start = self.parse_expr(0)?;
7212            if !matches!(self.peek(), Token::DotDot) {
7213                return Err(self.err(alloc::format!(
7214                    "expected '..' between FOR loop bounds, got {:?}",
7215                    self.peek()
7216                )));
7217            }
7218            self.advance();
7219            let end = self.parse_expr(0)?;
7220            let loop_kw = self.expect_ident_like()?;
7221            if !loop_kw.eq_ignore_ascii_case("loop") {
7222                return Err(self.err(alloc::format!(
7223                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7224                )));
7225            }
7226            let body = self.parse_plpgsql_stmt_list_until_end()?;
7227            let end_kw = self.expect_ident_like()?;
7228            if !end_kw.eq_ignore_ascii_case("end") {
7229                return Err(self.err(alloc::format!(
7230                    "expected END LOOP after FOR body, got {end_kw:?}"
7231                )));
7232            }
7233            let loop_kw2 = self.expect_ident_like()?;
7234            if !loop_kw2.eq_ignore_ascii_case("loop") {
7235                return Err(self.err(alloc::format!(
7236                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7237                )));
7238            }
7239            return Ok(PlPgSqlStmt::ForRange {
7240                var,
7241                start,
7242                end,
7243                reverse,
7244                body,
7245            });
7246        }
7247        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7248        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7249        {
7250            self.advance();
7251            let body = self.parse_plpgsql_stmt_list_until_end()?;
7252            let end_kw = self.expect_ident_like()?;
7253            if !end_kw.eq_ignore_ascii_case("end") {
7254                return Err(self.err(alloc::format!(
7255                    "expected END LOOP after LOOP body, got {end_kw:?}"
7256                )));
7257            }
7258            let loop_kw = self.expect_ident_like()?;
7259            if !loop_kw.eq_ignore_ascii_case("loop") {
7260                return Err(self.err(alloc::format!(
7261                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7262                )));
7263            }
7264            return Ok(PlPgSqlStmt::Loop { body });
7265        }
7266        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7267        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7268        {
7269            self.advance();
7270            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7271            {
7272                self.advance();
7273                Some(self.parse_expr(0)?)
7274            } else {
7275                None
7276            };
7277            return Ok(PlPgSqlStmt::Exit { when });
7278        }
7279        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7280        // already-parsed Statement or a runtime-computed SQL string.
7281        // The disambiguator vs the extended-query-protocol `EXECUTE
7282        // <stmt_name>` (which is a top-level Statement, not a
7283        // plpgsql line) is that inside a DO block / trigger body the
7284        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7285        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7286        {
7287            self.advance();
7288            let sql = self.parse_expr(0)?;
7289            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7290        }
7291        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7292        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7293        {
7294            self.advance();
7295            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7296            {
7297                self.advance();
7298                Some(self.parse_expr(0)?)
7299            } else {
7300                None
7301            };
7302            return Ok(PlPgSqlStmt::Continue { when });
7303        }
7304        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7305        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7306        {
7307            self.advance();
7308            let condition = self.parse_expr(0)?;
7309            let loop_kw = self.expect_ident_like()?;
7310            if !loop_kw.eq_ignore_ascii_case("loop") {
7311                return Err(self.err(alloc::format!(
7312                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7313                )));
7314            }
7315            let body = self.parse_plpgsql_stmt_list_until_end()?;
7316            // Expect END LOOP.
7317            let end_kw = self.expect_ident_like()?;
7318            if !end_kw.eq_ignore_ascii_case("end") {
7319                return Err(self.err(alloc::format!(
7320                    "expected END LOOP after WHILE body, got {end_kw:?}"
7321                )));
7322            }
7323            let loop_kw2 = self.expect_ident_like()?;
7324            if !loop_kw2.eq_ignore_ascii_case("loop") {
7325                return Err(self.err(alloc::format!(
7326                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7327                )));
7328            }
7329            return Ok(PlPgSqlStmt::While { condition, body });
7330        }
7331        // v7.12.6 — RAISE.
7332        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7333        {
7334            self.advance();
7335            return self.parse_plpgsql_raise();
7336        }
7337        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7338        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7339        {
7340            self.advance();
7341            let condition = self.parse_expr(0)?;
7342            let message = if matches!(self.peek(), Token::Comma) {
7343                self.advance();
7344                Some(self.parse_expr(0)?)
7345            } else {
7346                None
7347            };
7348            return Ok(PlPgSqlStmt::Assert { condition, message });
7349        }
7350        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7351        //   "PERFORM is equivalent to SELECT but discards the
7352        //    result." Side effects (function calls, RAISE inside
7353        //    SQL functions, etc.) still execute. We desugar to
7354        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7355        //    existing embedded-statement path handles execution +
7356        //    result-discard cleanly. The result is naturally
7357        //    discarded because EmbeddedSql doesn't propagate row
7358        //    sets back to the plpgsql interpreter.
7359        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7360        {
7361            self.advance();
7362            // Splice a synthetic Token::Select into the stream at
7363            // the current position so parse_select_stmt parses the
7364            // remainder as a normal SELECT body. Token-stream
7365            // surgery mirrors the try_parse_plpgsql_select_into
7366            // pattern used for SELECT … INTO desugaring.
7367            self.tokens.insert(self.pos, Token::Select);
7368            let select = self.parse_select_stmt()?;
7369            let Statement::Select(s) = select else {
7370                return Err(self.err(alloc::format!(
7371                    "expected SELECT body after PERFORM, got {:?}",
7372                    self.peek()
7373                )));
7374            };
7375            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7376        }
7377        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7378        // plpgsql-specific shape (mailrs round-10 migrate-042).
7379        // PG's SELECT INTO at top-level SQL would CREATE a new
7380        // table; inside plpgsql it ASSIGNS the query result to
7381        // a local variable. We detect the INTO at paren-depth
7382        // 0 between SELECT and the statement boundary; if
7383        // found, split the token stream into "pre-INTO
7384        // projection" + "var" + "post-INTO FROM/WHERE…" and
7385        // rebuild as a SelectInto with a regular SELECT body
7386        // (no INTO clause).
7387        if matches!(self.peek(), Token::Select)
7388            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7389        {
7390            return Ok(PlPgSqlStmt::SelectInto {
7391                var: var_name,
7392                body: Box::new(select_body),
7393            });
7394        }
7395        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7396        // SELECT can appear directly inside a trigger body; we
7397        // recurse into the regular Statement parser, which will
7398        // stop at the trailing `;` (which our caller then
7399        // consumes).
7400        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7401        // also embed ALTER / CREATE / DROP statements; route
7402        // those through the same parser so the DO body parses
7403        // cleanly.
7404        if matches!(self.peek(), Token::Insert)
7405            || matches!(self.peek(), Token::Select)
7406            || matches!(self.peek(), Token::Create)
7407            || matches!(self.peek(), Token::Drop)
7408            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7409                if s.eq_ignore_ascii_case("update")
7410                    || s.eq_ignore_ascii_case("delete")
7411                    || s.eq_ignore_ascii_case("alter"))
7412        {
7413            let stmt = self.parse_one_statement()?;
7414            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7415        }
7416        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7417        // followed by `:=` and an expression.
7418        let target = self.parse_plpgsql_assign_target()?;
7419        // PL/pgSQL assignment uses `:=`. The lexer represents
7420        // this as a colon followed by `=`; check both shapes.
7421        match self.peek() {
7422            Token::ColonEq => {
7423                self.advance();
7424            }
7425            Token::Colon => {
7426                self.advance();
7427                if !matches!(self.peek(), Token::Eq) {
7428                    return Err(self.err(alloc::format!(
7429                        "expected := after plpgsql assign target, got `:` then {:?}",
7430                        self.peek()
7431                    )));
7432                }
7433                self.advance();
7434            }
7435            other => {
7436                return Err(self.err(alloc::format!(
7437                    "expected := after plpgsql assign target, got {other:?}"
7438                )));
7439            }
7440        }
7441        let value = self.parse_expr(0)?;
7442        Ok(PlPgSqlStmt::Assign { target, value })
7443    }
7444
7445    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7446    /// [ELSE body] END IF`. `IF` keyword already consumed.
7447    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7448        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7449        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7450        loop {
7451            // <expr> THEN
7452            let cond = self.parse_expr(0)?;
7453            let then_kw = self.expect_ident_like()?;
7454            if !then_kw.eq_ignore_ascii_case("then") {
7455                return Err(self.err(alloc::format!(
7456                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7457                )));
7458            }
7459            let body = self.parse_plpgsql_stmt_list_until_end()?;
7460            branches.push((cond, body));
7461            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7462            match self.peek() {
7463                Token::Ident(s) | Token::QuotedIdent(s)
7464                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7465                {
7466                    self.advance();
7467                    continue;
7468                }
7469                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7470                    self.advance();
7471                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7472                    break;
7473                }
7474                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7475                    break;
7476                }
7477                other => {
7478                    return Err(self.err(alloc::format!(
7479                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7480                    )));
7481                }
7482            }
7483        }
7484        // Expect `END IF` (the END keyword is the one we're
7485        // looking at right now).
7486        let end_kw = self.expect_ident_like()?;
7487        if !end_kw.eq_ignore_ascii_case("end") {
7488            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7489        }
7490        let if_kw = self.expect_ident_like()?;
7491        if !if_kw.eq_ignore_ascii_case("if") {
7492            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7493        }
7494        Ok(PlPgSqlStmt::If {
7495            branches,
7496            else_branch,
7497        })
7498    }
7499
7500    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7501    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7502    /// is already consumed.
7503    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7504        let lvl_ident = self.expect_ident_like()?;
7505        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7506            "notice" => RaiseLevel::Notice,
7507            "warning" => RaiseLevel::Warning,
7508            "info" => RaiseLevel::Info,
7509            "log" => RaiseLevel::Log,
7510            "debug" => RaiseLevel::Debug,
7511            "exception" => RaiseLevel::Exception,
7512            other => {
7513                return Err(self.err(alloc::format!(
7514                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7515                )));
7516            }
7517        };
7518        // Message: required for v7.12.6. PG accepts a bare
7519        // RAISE-rethrow form (no message), reserved for future
7520        // RAISE-no-args support.
7521        let Token::String(msg) = self.peek() else {
7522            return Err(self.err(alloc::format!(
7523                "expected RAISE message string, got {:?}",
7524                self.peek()
7525            )));
7526        };
7527        let message = msg.clone();
7528        self.advance();
7529        // Optional comma-separated args (PG `%` format substitution).
7530        let mut args: Vec<Expr> = Vec::new();
7531        while matches!(self.peek(), Token::Comma) {
7532            self.advance();
7533            args.push(self.parse_expr(0)?);
7534        }
7535        Ok(PlPgSqlStmt::Raise {
7536            level,
7537            message,
7538            args,
7539        })
7540    }
7541
7542    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7543    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7544    /// migrate-042). Returns `(rebuilt_select_without_into,
7545    /// var_name)` when the pattern matches; `None` for
7546    /// regular SELECTs (those go through the embedded-SQL
7547    /// path). Token-stream surgery so the rebuilt SELECT
7548    /// parses through the regular `parse_select_stmt`.
7549    #[allow(clippy::too_many_lines)]
7550    fn try_parse_plpgsql_select_into(
7551        &mut self,
7552    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7553        // Scan forward from `self.pos + 1` (past Token::Select)
7554        // for Token::Into at paren-depth 0, stopping at the
7555        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7556        // end the plpgsql statement.
7557        let start = self.pos;
7558        let mut into_pos: Option<usize> = None;
7559        let mut depth: i32 = 0;
7560        let mut i = start + 1;
7561        while i < self.tokens.len() {
7562            match &self.tokens[i] {
7563                Token::LParen => depth += 1,
7564                Token::RParen => depth -= 1,
7565                Token::Semicolon if depth == 0 => break,
7566                Token::Ident(s)
7567                    if depth == 0
7568                        && (s.eq_ignore_ascii_case("end")
7569                            || s.eq_ignore_ascii_case("else")
7570                            || s.eq_ignore_ascii_case("elsif")) =>
7571                {
7572                    break;
7573                }
7574                Token::Into if depth == 0 => {
7575                    into_pos = Some(i);
7576                    break;
7577                }
7578                _ => {}
7579            }
7580            i += 1;
7581        }
7582        let Some(into_at) = into_pos else {
7583            return Ok(None);
7584        };
7585        // The token immediately after INTO must be the target
7586        // var ident; anything else (e.g. INSERT INTO table)
7587        // ruled out by the depth-0 check above. Capture it.
7588        let var = match self.tokens.get(into_at + 1) {
7589            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7590            other => {
7591                return Err(self.err(alloc::format!(
7592                    "expected variable name after SELECT … INTO, got {other:?}"
7593                )));
7594            }
7595        };
7596        // Find the end of the plpgsql SELECT INTO statement —
7597        // same boundary rules as the depth-0 scan above.
7598        let mut end = into_at + 2;
7599        let mut depth2: i32 = 0;
7600        while end < self.tokens.len() {
7601            match &self.tokens[end] {
7602                Token::LParen => depth2 += 1,
7603                Token::RParen => depth2 -= 1,
7604                Token::Semicolon if depth2 == 0 => break,
7605                Token::Ident(s)
7606                    if depth2 == 0
7607                        && (s.eq_ignore_ascii_case("end")
7608                            || s.eq_ignore_ascii_case("else")
7609                            || s.eq_ignore_ascii_case("elsif")) =>
7610                {
7611                    break;
7612                }
7613                _ => {}
7614            }
7615            end += 1;
7616        }
7617        // Rebuild a token stream that represents the SELECT
7618        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7619        // post-var tokens up to statement end]. Run the
7620        // regular `parse_select_stmt` against it.
7621        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7622        for j in start..into_at {
7623            rebuilt.push(self.tokens[j].clone());
7624        }
7625        for j in (into_at + 2)..end {
7626            rebuilt.push(self.tokens[j].clone());
7627        }
7628        rebuilt.push(Token::Eof);
7629        let saved_pos = self.pos;
7630        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7631        self.pos = 0;
7632        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7633        if !matches!(self.peek(), Token::Select) {
7634            self.tokens = saved_tokens;
7635            self.pos = saved_pos;
7636            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7637        }
7638        let sel = self.parse_select_stmt();
7639        self.tokens = saved_tokens;
7640        self.pos = end;
7641        let sel = sel?;
7642        let Statement::Select(body) = sel else {
7643            return Err(self.err(alloc::format!(
7644                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7645            )));
7646        };
7647        Ok(Some((body, var)))
7648    }
7649
7650    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7651        // v7.16.1 — read the head token DIRECTLY rather than
7652        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7653        // strip (`public.t` → `t`) inside `expect_ident_like`
7654        // greedily consumes any `ident . ident` pair, which
7655        // silently turned every `NEW.col := …` /
7656        // `OLD.col := …` plpgsql assignment into a Local("col")
7657        // assignment — the head "new"/"old" was eaten as if it
7658        // were a schema name and the Dot was consumed too, so
7659        // this function's own `peek() == Token::Dot` check
7660        // below never fired. Every BEFORE trigger that rewrote
7661        // a NEW cell was a silent no-op for two major releases
7662        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7663        // gate failures were investigated as v7.16.1 backlog.
7664        let head = match self.advance() {
7665            Token::Ident(s) | Token::QuotedIdent(s) => s,
7666            other => {
7667                return Err(self.err(alloc::format!(
7668                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7669                )));
7670            }
7671        };
7672        if matches!(self.peek(), Token::Dot) {
7673            self.advance();
7674            let col = self.expect_ident_like()?;
7675            if head.eq_ignore_ascii_case("new") {
7676                return Ok(AssignTarget::NewColumn(col));
7677            }
7678            if head.eq_ignore_ascii_case("old") {
7679                return Ok(AssignTarget::OldColumn(col));
7680            }
7681            return Err(self.err(alloc::format!(
7682                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7683                 got {head:?}.<col>"
7684            )));
7685        }
7686        Ok(AssignTarget::Local(head))
7687    }
7688
7689    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7690        // RETURN NEW / OLD / NULL — bare-ident forms.
7691        match self.peek() {
7692            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7693                self.advance();
7694                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7695            }
7696            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7697                self.advance();
7698                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7699            }
7700            Token::Null => {
7701                self.advance();
7702                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7703            }
7704            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7705            // per PL/pgSQL convention.
7706            Token::Semicolon => {
7707                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7708            }
7709            _ => {}
7710        }
7711        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7712        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7713        // caller-visible effect (blocks don't return sets), so we
7714        // desugar it identically to PERFORM: parse the SELECT (or
7715        // EXECUTE dynamic) as embedded SQL that runs for side
7716        // effects and discards the result. RETURN NEXT <expr>
7717        // (single-row accumulator) queues with v7.40 SETOF function
7718        // infrastructure.
7719        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7720        // and keep going.
7721        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7722        {
7723            self.advance();
7724            let e = self.parse_expr(0)?;
7725            return Ok(PlPgSqlStmt::ReturnNext(e));
7726        }
7727        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7728        {
7729            self.advance();
7730            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7731            // rows go to the set, like the static form. It used to desugar to a
7732            // bare ExecuteDynamic, whose result was DISCARDED.
7733            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7734            {
7735                self.advance();
7736                let sql = self.parse_expr(0)?;
7737                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7738            }
7739            // Bare RETURN QUERY <select>. If the current token is
7740            // not already SELECT (e.g., the user wrote `RETURN QUERY
7741            // <projection> FROM ...` in a shorthand — rare but PG
7742            // accepts a bare projection here), splice one in. Same
7743            // trick as PERFORM.
7744            if !matches!(self.peek(), Token::Select) {
7745                self.tokens.insert(self.pos, Token::Select);
7746            }
7747            let select = self.parse_select_stmt()?;
7748            let Statement::Select(s) = select else {
7749                return Err(self.err(alloc::format!(
7750                    "expected SELECT body after RETURN QUERY, got {:?}",
7751                    self.peek()
7752                )));
7753            };
7754            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7755            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7756            // in a SETOF function is the entire answer thrown away.
7757            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7758        }
7759        // Fall through: parse a full expression.
7760        let e = self.parse_expr(0)?;
7761        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7762    }
7763
7764    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7765        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7766        // are ident-shaped (the parser keys off case-insensitive
7767        // match — same shape used by the top-level Update / Delete
7768        // dispatchers at parse_one_statement).
7769        if matches!(self.peek(), Token::Insert) {
7770            self.advance();
7771            return Ok(TriggerEvent::Insert);
7772        }
7773        match self.peek() {
7774            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7775                self.advance();
7776                Ok(TriggerEvent::Update)
7777            }
7778            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7779                self.advance();
7780                Ok(TriggerEvent::Delete)
7781            }
7782            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7783                self.advance();
7784                Ok(TriggerEvent::Truncate)
7785            }
7786            other => Err(self.err(alloc::format!(
7787                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7788            ))),
7789        }
7790    }
7791
7792    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7793    ///   - (no clause) → implicit `FOR ALL TABLES`
7794    ///   - `FOR ALL TABLES`
7795    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7796    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7797    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7798    ///     REJECTS the bare plural (`invalid publication object list`,
7799    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7800    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7801    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7802        let name = self.expect_ident_or_string()?;
7803        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7804        // shape so existing publications keep parsing identically.
7805        let scope = if matches!(self.peek(), Token::For) {
7806            self.advance();
7807            if matches!(self.peek(), Token::All) {
7808                self.advance();
7809                if !matches!(self.peek(), Token::Tables) {
7810                    return Err(self.err(format!(
7811                        "expected TABLES after FOR ALL, got {:?}",
7812                        self.peek()
7813                    )));
7814                }
7815                self.advance();
7816                if matches!(self.peek(), Token::Except) {
7817                    self.advance();
7818                    let tables = self.parse_publication_table_list()?;
7819                    PublicationScope::AllTablesExcept(tables)
7820                } else {
7821                    PublicationScope::AllTables
7822                }
7823            } else if matches!(self.peek(), Token::Table) {
7824                self.advance();
7825                let tables = self.parse_publication_table_list()?;
7826                PublicationScope::ForTables(tables)
7827            } else if matches!(self.peek(), Token::Tables) {
7828                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7829                // plural (`FOR TABLES t`) is REJECTED (`invalid
7830                // publication object list`); TABLES only pairs with
7831                // `IN SCHEMA`. The old arm accepted it on an
7832                // unverifiable "PG 19 accepts both" claim.
7833                self.advance();
7834                if !matches!(self.peek(), Token::In) {
7835                    return Err(self.err(alloc::string::String::from(
7836                        "invalid publication object list",
7837                    )));
7838                }
7839                self.advance();
7840                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7841                    return Err(self.err(format!(
7842                        "expected SCHEMA after FOR TABLES IN, got {:?}",
7843                        self.peek()
7844                    )));
7845                }
7846                self.advance();
7847                let schema = self.expect_ident_or_string()?;
7848                PublicationScope::TablesInSchema(schema)
7849            } else {
7850                return Err(self.err(format!(
7851                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7852                    self.peek()
7853                )));
7854            }
7855        } else {
7856            PublicationScope::AllTables
7857        };
7858        Ok(Statement::CreatePublication(CreatePublicationStatement {
7859            name,
7860            scope,
7861        }))
7862    }
7863
7864    /// v6.1.3 — Comma-separated identifier list for the publication
7865    /// FOR-clause. Requires at least one entry; empty list is a
7866    /// parse error (PG behaviour). Quoted idents are accepted; the
7867    /// names round-trip through `Display` as `quote_ident(name)`.
7868    ///
7869    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7870    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7871    /// pg_dump output. SPG's publication state today is per-table
7872    /// only (matching the pre-PG-15 surface); the col list + WHERE
7873    /// are parsed so dumps load through and the table name reaches
7874    /// `PublicationScope::ForTables`, but the filter is not enforced
7875    /// at publish time. Re-open when a customer dogfood gate
7876    /// requires per-row-filter or column-subset publish semantics
7877    /// (which gates on persistent slot state landing first, 21.12).
7878    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7879        let first = self.parse_publication_table_entry()?;
7880        let mut out = alloc::vec![first];
7881        while matches!(self.peek(), Token::Comma) {
7882            self.advance();
7883            out.push(self.parse_publication_table_entry()?);
7884        }
7885        Ok(out)
7886    }
7887
7888    /// One table entry inside a FOR TABLE clause:
7889    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7890    /// Returns just the table name; the column list + WHERE predicate
7891    /// are consumed and discarded per the parse-accept-discard
7892    /// commitment above.
7893    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7894        let name = self.expect_ident_like()?;
7895        // Optional column list — `(col, col, …)`.
7896        if matches!(self.peek(), Token::LParen) {
7897            self.advance();
7898            // Empty parens are a PG error too; require ≥ 1 column.
7899            let _ = self.expect_ident_like()?;
7900            while matches!(self.peek(), Token::Comma) {
7901                self.advance();
7902                let _ = self.expect_ident_like()?;
7903            }
7904            if !matches!(self.peek(), Token::RParen) {
7905                return Err(self.err(alloc::format!(
7906                    "expected ')' to close publication column list, got {:?}",
7907                    self.peek()
7908                )));
7909            }
7910            self.advance();
7911        }
7912        // Optional row filter — `WHERE (predicate)`.
7913        if matches!(self.peek(), Token::Where) {
7914            self.advance();
7915            if !matches!(self.peek(), Token::LParen) {
7916                return Err(self.err(alloc::format!(
7917                    "expected '(' after WHERE in publication row filter, got {:?}",
7918                    self.peek()
7919                )));
7920            }
7921            self.advance();
7922            let _ = self.parse_expr(0)?;
7923            if !matches!(self.peek(), Token::RParen) {
7924                return Err(self.err(alloc::format!(
7925                    "expected ')' to close publication WHERE filter, got {:?}",
7926                    self.peek()
7927                )));
7928            }
7929            self.advance();
7930        }
7931        Ok(name)
7932    }
7933
7934    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7935    ///                 CONNECTION '<conn>'
7936    ///                 PUBLICATION <pub> [, <pub> ...]`.
7937    ///
7938    /// The clause order is fixed (CONNECTION first, then
7939    /// PUBLICATION) to match PG. No WITH-options accepted in
7940    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7941    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7942        let name = self.expect_ident_or_string()?;
7943        if !matches!(self.peek(), Token::Connection) {
7944            return Err(self.err(format!(
7945                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7946                self.peek()
7947            )));
7948        }
7949        self.advance();
7950        let conn_str = self.expect_string_literal()?;
7951        if !matches!(self.peek(), Token::Publication) {
7952            return Err(self.err(format!(
7953                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7954                self.peek()
7955            )));
7956        }
7957        self.advance();
7958        // Reuse the publication FOR-list parser shape: at least one
7959        // identifier, comma-separated.
7960        let first = self.expect_ident_like()?;
7961        let mut publications = alloc::vec![first];
7962        while matches!(self.peek(), Token::Comma) {
7963            self.advance();
7964            publications.push(self.expect_ident_like()?);
7965        }
7966        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7967            name,
7968            conn_str,
7969            publications,
7970        }))
7971    }
7972
7973    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7974    /// All keywords after `WAIT` are bare idents in v6.1.x; no
7975    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7976    /// that fit `u64`.
7977    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7978    /// qualifier is a *namespace* the app owns (`app.user_id`,
7979    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7980    /// to discard. So parse the raw segments here instead of
7981    /// `expect_ident_like`, which strips a leading `schema.` qualifier
7982    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7983    /// a single segment and round-trip unchanged.
7984    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7985        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7986        loop {
7987            let seg = match self.advance() {
7988                Token::Ident(s) | Token::QuotedIdent(s) => s,
7989                other if unreserved_keyword_text(&other).is_some() => {
7990                    unreserved_keyword_text(&other).unwrap()
7991                }
7992                other => {
7993                    return Err(ParseError {
7994                        message: format!("expected parameter name, got {other:?}"),
7995                        token_pos: self.consumed_pos(),
7996                    });
7997                }
7998            };
7999            parts.push(seg);
8000            if matches!(self.peek(), Token::Dot) {
8001                self.advance();
8002                continue;
8003            }
8004            break;
8005        }
8006        Ok(parts.join(".").to_ascii_lowercase())
8007    }
8008
8009    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8010        Self::parse_set_value_inner(self)
8011    }
8012
8013    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8014        match self.advance() {
8015            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8016            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8017                Ok(crate::ast::SetValue::Default)
8018            }
8019            Token::Ident(s) | Token::QuotedIdent(s) => {
8020                let mut accum = s;
8021                while matches!(self.peek(), Token::Dot) {
8022                    self.advance();
8023                    let next = self.expect_ident_like()?;
8024                    accum.push('.');
8025                    accum.push_str(&next);
8026                }
8027                Ok(crate::ast::SetValue::Ident(accum))
8028            }
8029            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8030            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8031            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8032            // spellings that lex as keyword tokens, not idents:
8033            // `SET standard_conforming_strings = on` is in every
8034            // pg_dump preamble (`off` already lexes as an ident).
8035            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8036            // DEFAULT lexes as its keyword token, so the ident arm above
8037            // never saw it and the everyday reset form was a syntax error.
8038            Token::Default => Ok(crate::ast::SetValue::Default),
8039            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8040            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8041            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8042            // v7.14.0 — MySQL session/user variable RHS
8043            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8044            // Wrap as Ident so the SET handler can record it; the
8045            // engine treats `@VAR` / `@@VAR` values as opaque
8046            // strings.
8047            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8048            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8049            // is the common MySQL preamble shape. Allow a `+` or
8050            // `-` prefix on negative numerics for parity with PG
8051            // (some param defaults are negative).
8052            Token::Minus => match self.advance() {
8053                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8054                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8055                other => Err(self.err(format!(
8056                    "expected numeric after `-` in SET value, got {other:?}"
8057                ))),
8058            },
8059            other => Err(self.err(format!(
8060                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8061            ))),
8062        }
8063    }
8064
8065    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8066    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8067    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8068    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8069    /// present). Modes are comma-separated per PG; SPG also
8070    /// accepts space-separated for tolerance. READ ONLY / WRITE
8071    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8072    /// surface but not behaviorally honoured today).
8073    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8074    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8075    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8076    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8077    /// session default rather than forcing READ COMMITTED.
8078    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8079        let mut level = IsolationLevel::default();
8080        let mut have_level = false;
8081        loop {
8082            // ISOLATION LEVEL …
8083            let saw_isolation =
8084                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8085            if saw_isolation {
8086                self.advance(); // ISOLATION
8087                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8088                    return Err(self.err(alloc::format!(
8089                        "expected LEVEL after ISOLATION, got {:?}",
8090                        self.peek()
8091                    )));
8092                }
8093                self.advance(); // LEVEL
8094                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8095                let w1 = self
8096                    .expect_ident_like()
8097                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8098                let lc = w1.to_ascii_lowercase();
8099                level = match lc.as_str() {
8100                    "serializable" => IsolationLevel::Serializable,
8101                    "repeatable" => {
8102                        // Expect READ
8103                        let w2 = self
8104                            .expect_ident_like()
8105                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8106                        if !w2.eq_ignore_ascii_case("read") {
8107                            return Err(self.err(alloc::format!(
8108                                "expected READ after REPEATABLE, got {w2:?}"
8109                            )));
8110                        }
8111                        IsolationLevel::RepeatableRead
8112                    }
8113                    "read" => {
8114                        let w2 = self
8115                            .expect_ident_like()
8116                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8117                        match w2.to_ascii_lowercase().as_str() {
8118                            "committed" => IsolationLevel::ReadCommitted,
8119                            "uncommitted" => IsolationLevel::ReadUncommitted,
8120                            other => {
8121                                return Err(self.err(alloc::format!(
8122                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8123                                )));
8124                            }
8125                        }
8126                    }
8127                    other => {
8128                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8129                    }
8130                };
8131                have_level = true;
8132            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8133                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8134                self.advance();
8135                match self.peek().clone() {
8136                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8137                        self.advance();
8138                    }
8139                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8140                        self.advance();
8141                    }
8142                    other => {
8143                        return Err(self.err(alloc::format!(
8144                            "expected ONLY or WRITE after READ, got {other:?}"
8145                        )));
8146                    }
8147                }
8148            } else if matches!(self.peek(), Token::Not) {
8149                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8150                self.advance();
8151                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8152                    return Err(self.err(alloc::format!(
8153                        "expected DEFERRABLE after NOT, got {:?}",
8154                        self.peek()
8155                    )));
8156                }
8157                self.advance();
8158            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8159            {
8160                self.advance();
8161            } else {
8162                break;
8163            }
8164            // Optional comma between modes.
8165            if matches!(self.peek(), Token::Comma) {
8166                self.advance();
8167            }
8168        }
8169        Ok(have_level.then_some(level))
8170    }
8171
8172    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8173        // FOR is a v6.1.2-reserved keyword (Token::For). The
8174        // other two are bare idents — they've never needed lexer
8175        // support and we keep it that way.
8176        if !matches!(self.peek(), Token::For) {
8177            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8178        }
8179        self.advance();
8180        self.expect_keyword_ident("wal")?;
8181        self.expect_keyword_ident("position")?;
8182        let pos = self.expect_u64_literal()?;
8183        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8184        {
8185            self.advance();
8186            self.expect_keyword_ident("timeout")?;
8187            Some(self.expect_u64_literal()?)
8188        } else {
8189            None
8190        };
8191        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8192    }
8193
8194    /// v6.1.7 helper — consume a `Token::Integer` and check it
8195    /// fits `u64`. WAL positions and millisecond timeouts are
8196    /// non-negative.
8197    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8198        match self.advance() {
8199            Token::Integer(n) if n >= 0 => Ok(n as u64),
8200            Token::Integer(n) => Err(ParseError {
8201                message: format!("expected non-negative integer, got {n}"),
8202                token_pos: self.consumed_pos(),
8203            }),
8204            other => Err(ParseError {
8205                message: format!("expected integer literal, got {other:?}"),
8206                token_pos: self.consumed_pos(),
8207            }),
8208        }
8209    }
8210
8211    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8212    /// ROLE '<role>' (defaults to readonly). All string slots accept
8213    /// either a quoted ident or a quoted string literal.
8214    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8215    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8216    ///
8217    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8218    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8219    /// wire role) still parses — it is a different axis from the PG attributes.
8220    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8221    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8222    /// or RESET, so the plain attribute forms keep their old path.
8223    fn peeks_db_role_setting(&self) -> bool {
8224        let mut i = self.pos + 1; // past the object's name
8225        let word = |p: usize| -> Option<String> {
8226            match self.tokens.get(p) {
8227                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8228                Some(Token::In) => Some(String::from("in")),
8229                _ => None,
8230            }
8231        };
8232        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8233            i += 3; // IN DATABASE <name>
8234        }
8235        matches!(word(i).as_deref(), Some("set" | "reset"))
8236    }
8237
8238    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8239        use crate::ast::SetDbRoleSettingStatement;
8240        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8241        // identifier, so the ordinary name reader refuses it. Same trap
8242        // as TABLE / INDEX / FULL / DEFAULT before it.
8243        let name = if matches!(self.peek(), Token::All) {
8244            self.advance();
8245            String::from("all")
8246        } else {
8247            self.expect_ident_or_string()?
8248        };
8249        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8250        let all = name.eq_ignore_ascii_case("all");
8251        let (mut database, mut role) = if is_database {
8252            (Some(name), None)
8253        } else if all {
8254            (None, None)
8255        } else {
8256            (None, Some(name))
8257        };
8258        if matches!(self.peek(), Token::In) {
8259            self.advance();
8260            self.advance(); // DATABASE
8261            database = Some(self.expect_ident_or_string()?);
8262        }
8263        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8264        self.advance(); // SET | RESET
8265        if resetting && matches!(self.peek(), Token::All) {
8266            self.advance();
8267            self.consume_until_statement_boundary();
8268            return Ok(Statement::SetDbRoleSetting(Box::new(
8269                SetDbRoleSettingStatement {
8270                    database,
8271                    role,
8272                    param: None,
8273                    value: None,
8274                },
8275            )));
8276        }
8277        let param = self.expect_ident_like()?;
8278        let value = if resetting {
8279            None
8280        } else {
8281            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8282            // KEYWORD, so the ident-only check missed it and consumed
8283            // the word itself as the value — the same trap as ALL, one
8284            // clause over.
8285            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8286                self.advance();
8287            }
8288            Some(self.take_guc_value())
8289        };
8290        self.consume_until_statement_boundary();
8291        Ok(Statement::SetDbRoleSetting(Box::new(
8292            SetDbRoleSettingStatement {
8293                database,
8294                role,
8295                param: Some(param),
8296                value,
8297            },
8298        )))
8299    }
8300
8301    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8302    /// a quoted literal loses its quotes, a bare word or number does not.
8303    fn take_guc_value(&mut self) -> String {
8304        match self.advance() {
8305            Token::String(s) => s,
8306            Token::Integer(n) => format!("{n}"),
8307            Token::Float(f) => format!("{f}"),
8308            Token::Ident(s) | Token::QuotedIdent(s) => s,
8309            other => format!("{other:?}"),
8310        }
8311    }
8312
8313    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8314        let name = self.expect_ident_or_string()?;
8315        if self.peek_keyword_ident("with") {
8316            self.advance();
8317        }
8318        let mut password = String::new();
8319        let mut role = String::new();
8320        let mut login: Option<bool> = None;
8321        let mut inherit: Option<bool> = None;
8322        let mut superuser: Option<bool> = None;
8323        // Not a `while let`: the pattern would borrow `self` across the
8324        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8325        #[allow(clippy::while_let_loop)]
8326        loop {
8327            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8328                break;
8329            };
8330            match w.to_ascii_lowercase().as_str() {
8331                "password" => {
8332                    self.advance();
8333                    password = self.expect_string_literal()?;
8334                }
8335                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8336                // is the same slot.
8337                "encrypted" => {
8338                    self.advance();
8339                    self.expect_keyword_ident("password")?;
8340                    password = self.expect_string_literal()?;
8341                }
8342                "login" => {
8343                    self.advance();
8344                    login = Some(true);
8345                }
8346                "nologin" => {
8347                    self.advance();
8348                    login = Some(false);
8349                }
8350                "inherit" => {
8351                    self.advance();
8352                    inherit = Some(true);
8353                }
8354                "noinherit" => {
8355                    self.advance();
8356                    inherit = Some(false);
8357                }
8358                "superuser" => {
8359                    self.advance();
8360                    superuser = Some(true);
8361                }
8362                "nosuperuser" => {
8363                    self.advance();
8364                    superuser = Some(false);
8365                }
8366                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8367                "role" => {
8368                    self.advance();
8369                    role = self.expect_string_literal()?;
8370                }
8371                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8372                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8373                // accepted and ignored so a pg_dump role block restores. They
8374                // gate capabilities SPG does not have.
8375                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8376                | "noreplication" | "bypassrls" | "nobypassrls" => {
8377                    self.advance();
8378                }
8379                "connection" => {
8380                    self.advance();
8381                    self.expect_keyword_ident("limit")?;
8382                    self.advance(); // the number
8383                }
8384                "valid" => {
8385                    self.advance();
8386                    self.expect_keyword_ident("until")?;
8387                    self.expect_string_literal()?;
8388                }
8389                _ => break,
8390            }
8391        }
8392        if role.is_empty() {
8393            role = "readonly".to_string();
8394        }
8395        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8396            name,
8397            password,
8398            role,
8399            login,
8400            inherit,
8401            superuser,
8402            is_user,
8403        }))
8404    }
8405
8406    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8407    /// consumed the USING / WITH CHECK keyword.
8408    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8409        if !matches!(self.peek(), Token::LParen) {
8410            return Err(self.err(alloc::format!(
8411                "expected '(' after {clause}, got {:?}",
8412                self.peek()
8413            )));
8414        }
8415        self.advance();
8416        let e = self.parse_expr(0)?;
8417        if !matches!(self.peek(), Token::RParen) {
8418            return Err(self.err(alloc::format!(
8419                "expected ')' to close {clause}, got {:?}",
8420                self.peek()
8421            )));
8422        }
8423        self.advance();
8424        Ok(e)
8425    }
8426
8427    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8428    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8429        let mut roles = Vec::new();
8430        loop {
8431            roles.push(self.expect_ident_like()?);
8432            if matches!(self.peek(), Token::Comma) {
8433                self.advance();
8434            } else {
8435                break;
8436            }
8437        }
8438        Ok(roles)
8439    }
8440
8441    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8442    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8443    /// `CREATE POLICY`.
8444    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8445        use crate::ast::PolicyCmd;
8446        let name = self.expect_ident_like()?;
8447        if !matches!(self.peek(), Token::On) {
8448            return Err(self.err(alloc::format!(
8449                "expected ON after CREATE POLICY name, got {:?}",
8450                self.peek()
8451            )));
8452        }
8453        self.advance();
8454        let table = self.expect_ident_like()?;
8455
8456        let mut permissive = true;
8457        if matches!(self.peek(), Token::As) {
8458            self.advance();
8459            let w = self.expect_ident_like()?;
8460            permissive = if w.eq_ignore_ascii_case("permissive") {
8461                true
8462            } else if w.eq_ignore_ascii_case("restrictive") {
8463                false
8464            } else {
8465                return Err(self.err(alloc::format!(
8466                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8467                )));
8468            };
8469        }
8470
8471        let mut cmd = PolicyCmd::All;
8472        if matches!(self.peek(), Token::For) {
8473            self.advance();
8474            cmd = self.parse_policy_cmd()?;
8475        }
8476
8477        let mut roles = Vec::new();
8478        if matches!(self.peek(), Token::To) {
8479            self.advance();
8480            roles = self.parse_policy_roles()?;
8481        }
8482
8483        let mut using = None;
8484        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8485        {
8486            self.advance();
8487            using = Some(self.parse_paren_expr("USING")?);
8488        }
8489
8490        let mut with_check = None;
8491        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8492        {
8493            self.advance();
8494            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8495            {
8496                return Err(self.err(alloc::format!(
8497                    "expected CHECK after WITH, got {:?}",
8498                    self.peek()
8499                )));
8500            }
8501            self.advance();
8502            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8503        }
8504
8505        // Clause-per-command matrix (PG wording).
8506        match cmd {
8507            PolicyCmd::Insert => {
8508                if using.is_some() {
8509                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8510                }
8511            }
8512            PolicyCmd::Select | PolicyCmd::Delete => {
8513                if with_check.is_some() {
8514                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8515                }
8516            }
8517            PolicyCmd::Update | PolicyCmd::All => {}
8518        }
8519
8520        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8521            name,
8522            table,
8523            permissive,
8524            cmd,
8525            roles,
8526            using,
8527            with_check,
8528        }))
8529    }
8530
8531    /// v7.39 (RLS) — the command word after `FOR`.
8532    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8533        use crate::ast::PolicyCmd;
8534        match self.peek().clone() {
8535            Token::All => {
8536                self.advance();
8537                Ok(PolicyCmd::All)
8538            }
8539            Token::Select => {
8540                self.advance();
8541                Ok(PolicyCmd::Select)
8542            }
8543            Token::Insert => {
8544                self.advance();
8545                Ok(PolicyCmd::Insert)
8546            }
8547            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8548                self.advance();
8549                Ok(PolicyCmd::Update)
8550            }
8551            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8552                self.advance();
8553                Ok(PolicyCmd::Delete)
8554            }
8555            other => Err(self.err(alloc::format!(
8556                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8557            ))),
8558        }
8559    }
8560
8561    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8562    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8563    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8564        let name = self.expect_ident_like()?;
8565        if !matches!(self.peek(), Token::On) {
8566            return Err(self.err(alloc::format!(
8567                "expected ON after ALTER POLICY name, got {:?}",
8568                self.peek()
8569            )));
8570        }
8571        self.advance();
8572        let table = self.expect_ident_like()?;
8573
8574        // RENAME TO new
8575        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8576        {
8577            self.advance();
8578            if !matches!(self.peek(), Token::To) {
8579                return Err(self.err(alloc::format!(
8580                    "expected TO after RENAME, got {:?}",
8581                    self.peek()
8582                )));
8583            }
8584            self.advance();
8585            let new = self.expect_ident_like()?;
8586            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8587                name,
8588                table,
8589                rename_to: Some(new),
8590                roles: None,
8591                using: None,
8592                with_check: None,
8593            }));
8594        }
8595
8596        let mut roles = None;
8597        if matches!(self.peek(), Token::To) {
8598            self.advance();
8599            roles = Some(self.parse_policy_roles()?);
8600        }
8601        let mut using = None;
8602        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8603        {
8604            self.advance();
8605            using = Some(self.parse_paren_expr("USING")?);
8606        }
8607        let mut with_check = None;
8608        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8609        {
8610            self.advance();
8611            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8612            {
8613                return Err(self.err(alloc::format!(
8614                    "expected CHECK after WITH, got {:?}",
8615                    self.peek()
8616                )));
8617            }
8618            self.advance();
8619            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8620        }
8621        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8622            name,
8623            table,
8624            rename_to: None,
8625            roles,
8626            using,
8627            with_check,
8628        }))
8629    }
8630
8631    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8632    /// `DROP POLICY`.
8633    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8634        let if_exists = self.consume_if_exists();
8635        let name = self.expect_ident_like()?;
8636        if !matches!(self.peek(), Token::On) {
8637            return Err(self.err(alloc::format!(
8638                "expected ON after DROP POLICY name, got {:?}",
8639                self.peek()
8640            )));
8641        }
8642        self.advance();
8643        let table = self.expect_ident_like()?;
8644        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8645            name,
8646            table,
8647            if_exists,
8648        }))
8649    }
8650}
8651fn wrap_from_leaves(
8652    e: &mut Expr,
8653    names: &[String],
8654    make: &dyn Fn(Expr) -> Expr,
8655    refs: &dyn Fn(&Expr) -> bool,
8656) {
8657    if let Expr::Column(c) = e {
8658        if c.qualifier
8659            .as_deref()
8660            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8661        {
8662            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8663            *e = make(taken);
8664        }
8665        return;
8666    }
8667    match e {
8668        Expr::Binary { lhs, rhs, .. } => {
8669            wrap_from_leaves(lhs, names, make, refs);
8670            wrap_from_leaves(rhs, names, make, refs);
8671        }
8672        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8673            wrap_from_leaves(expr, names, make, refs)
8674        }
8675        Expr::FunctionCall { args, .. } => {
8676            for a in args.iter_mut() {
8677                wrap_from_leaves(a, names, make, refs);
8678            }
8679        }
8680        Expr::Case {
8681            operand,
8682            branches,
8683            else_branch,
8684        } => {
8685            if let Some(o) = operand.as_deref_mut() {
8686                wrap_from_leaves(o, names, make, refs);
8687            }
8688            for (w, t) in branches.iter_mut() {
8689                wrap_from_leaves(w, names, make, refs);
8690                wrap_from_leaves(t, names, make, refs);
8691            }
8692            if let Some(el) = else_branch.as_deref_mut() {
8693                wrap_from_leaves(el, names, make, refs);
8694            }
8695        }
8696        // Compound variants the walk doesn't decompose: keep the
8697        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8698        // a source table, so nothing regresses.
8699        other => {
8700            if refs(other) {
8701                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8702                *other = make(taken);
8703            }
8704        }
8705    }
8706}
8707
8708/// v7.39 (round 241) — does this expression reference any of the FROM /
8709/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8710/// lowerings)?
8711fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8712    match e {
8713        Expr::Column(c) => c
8714            .qualifier
8715            .as_deref()
8716            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8717        Expr::Binary { lhs, rhs, .. } => {
8718            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8719        }
8720        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8721        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8722        Expr::Case {
8723            operand,
8724            branches,
8725            else_branch,
8726        } => {
8727            operand
8728                .as_deref()
8729                .is_some_and(|o| expr_refs_tables(o, names))
8730                || branches
8731                    .iter()
8732                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8733                || else_branch
8734                    .as_deref()
8735                    .is_some_and(|el| expr_refs_tables(el, names))
8736        }
8737        _ => false,
8738    }
8739}
8740
8741impl Parser {
8742    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8743    /// Caller already consumed the leading `UPDATE` ident.
8744    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8745    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8746    /// after the target name has been read. `JOIN` is a reserved token;
8747    /// the qualifiers are bare idents.
8748    fn peek_is_update_join_start(&self) -> bool {
8749        match self.peek() {
8750            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8751            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8752            Token::Join
8753            | Token::Inner
8754            | Token::Left
8755            | Token::Right
8756            | Token::Cross
8757            | Token::Full => true,
8758            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8759            Token::Ident(s) | Token::QuotedIdent(s) => {
8760                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8761            }
8762            _ => false,
8763        }
8764    }
8765
8766    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8767    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8768    /// expression on the right, and `:=` as a second spelling of `=`.
8769    ///
8770    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8771    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8772    /// and holding this loop's `Vec` + `String` locals there overflowed the
8773    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8774    #[inline(never)]
8775    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8776        let mut assigns: Vec<(String, Expr)> = Vec::new();
8777        let mut settings: Vec<(String, Expr)> = Vec::new();
8778        loop {
8779            // v7.39 (round 554) — a plain NAME here is a session
8780            // setting, not a user variable. mysqldump writes the two in
8781            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8782            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8783            // changes it — and this refused the mixture outright, so no
8784            // dump could be restored past its preamble.
8785            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8786                self.advance();
8787                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8788                    return Err(self.err(alloc::format!(
8789                        "expected `=` after {name}, got {:?}",
8790                        self.peek()
8791                    )));
8792                }
8793                self.advance();
8794                let value = self.parse_expr(0)?;
8795                settings.push((name.to_ascii_lowercase(), value));
8796                if matches!(self.peek(), Token::Comma) {
8797                    self.advance();
8798                    continue;
8799                }
8800                break;
8801            }
8802            let Token::SessionVar(raw) = self.peek().clone() else {
8803                return Err(self.err(alloc::format!(
8804                    "expected a user variable after SET, got {:?}",
8805                    self.peek()
8806                )));
8807            };
8808            if raw.starts_with("@@") {
8809                return Err(self.err(alloc::string::String::from(
8810                    "cannot mix `@@` settings with `@` user variables in one SET",
8811                )));
8812            }
8813            self.advance();
8814            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8815                return Err(self.err(alloc::format!(
8816                    "expected `=` or `:=` after {raw}, got {:?}",
8817                    self.peek()
8818                )));
8819            }
8820            self.advance();
8821            let value = self.parse_expr(0)?;
8822            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8823            if matches!(self.peek(), Token::Comma) {
8824                self.advance();
8825                continue;
8826            }
8827            break;
8828        }
8829        Ok(Statement::SetUserVars(assigns, settings))
8830    }
8831
8832    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8833        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8834        // NAMED `only` until now, which failed on `relation "only" does
8835        // not exist`. The lookahead is what keeps a table actually
8836        // called `only` working: the keyword is only a keyword when a
8837        // TABLE NAME follows it — and `SET` arrives as an identifier
8838        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8839        // for the table and die on the `=`. Measured by the pin.
8840        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8841            if s.eq_ignore_ascii_case("only"))
8842            && matches!(
8843                self.tokens.get(self.pos + 1),
8844                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8845            );
8846        if only {
8847            self.advance();
8848        }
8849        let table = self.expect_ident_like()?;
8850        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8851        // bare spelling; a bare identifier that is the SET keyword itself
8852        // is the clause, not an alias.
8853        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8854        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8855        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8856        // following JOIN a syntax error.
8857        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8858        let alias = if matches!(self.peek(), Token::As) {
8859            self.advance();
8860            Some(self.expect_ident_like()?)
8861        } else {
8862            match self.peek() {
8863                Token::Ident(s) | Token::QuotedIdent(s)
8864                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
8865                {
8866                    let a = s.clone();
8867                    self.advance();
8868                    Some(a)
8869                }
8870                _ => None,
8871            }
8872        };
8873        // v7.39 (round 420) — MySQL's multi-table UPDATE:
8874        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
8875        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
8876        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8877        // The FIRST table is the mutation target and the rest are sources —
8878        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8879        // SPG already lowers onto correlated subqueries. So rewind, let
8880        // `parse_from_clause` read the whole list (it handles aliases, comma
8881        // lists, and every JOIN form), then peel the target off the front.
8882        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8883            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8884        {
8885            // NOTE: `advance()` destroys the tokens it returns
8886            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8887            // is NOT possible — the tail is read forward, once, through the
8888            // same grammar `parse_from_clause` uses after its primary.
8889            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8890            let mut joins = self.parse_from_joins(&target_qual)?;
8891            if joins.is_empty() {
8892                return Err(self.err(alloc::string::String::from(
8893                    "multi-table UPDATE needs at least one source table",
8894                )));
8895            }
8896            let head = joins.remove(0);
8897            // A LEFT join keeps every target row (the unmatched ones see NULL
8898            // on the source side), so it must NOT get the EXISTS row filter
8899            // the inner / comma forms use.
8900            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8901            let src = FromClause {
8902                primary: head.table,
8903                joins,
8904            };
8905            (Some(src), head.on, outer)
8906        } else {
8907            (None, None, false)
8908        };
8909        self.expect_keyword_ident("set")?;
8910        let mut assignments = Vec::new();
8911        loop {
8912            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8913            // …)` — the parenthesized multi-assignment. Expressions
8914            // assign positionally; a subquery RHS clones per column
8915            // keeping only the Nth projection item.
8916            if matches!(self.peek(), Token::LParen) {
8917                self.advance();
8918                let mut cols = alloc::vec![self.expect_ident_like()?];
8919                while matches!(self.peek(), Token::Comma) {
8920                    self.advance();
8921                    cols.push(self.expect_ident_like()?);
8922                }
8923                if !matches!(self.peek(), Token::RParen) {
8924                    return Err(self.err(format!(
8925                        "expected ')' after SET column list, got {:?}",
8926                        self.peek()
8927                    )));
8928                }
8929                self.advance();
8930                if !matches!(self.peek(), Token::Eq) {
8931                    return Err(self.err(format!(
8932                        "expected `=` after SET column list, got {:?}",
8933                        self.peek()
8934                    )));
8935                }
8936                self.advance();
8937                if !matches!(self.peek(), Token::LParen) {
8938                    return Err(self.err(format!(
8939                        "expected '(' after SET (…) =, got {:?}",
8940                        self.peek()
8941                    )));
8942                }
8943                self.advance();
8944                if matches!(self.peek(), Token::Select) {
8945                    let inner = match self.parse_select_stmt()? {
8946                        Statement::Select(s) => s,
8947                        other => {
8948                            return Err(self.err(alloc::format!(
8949                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8950                            )));
8951                        }
8952                    };
8953                    if !matches!(self.peek(), Token::RParen) {
8954                        return Err(self.err(format!(
8955                            "expected ')' after SET subquery, got {:?}",
8956                            self.peek()
8957                        )));
8958                    }
8959                    self.advance();
8960                    if inner.items.len() != cols.len() {
8961                        return Err(self.err(alloc::format!(
8962                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8963                            cols.len(),
8964                            inner.items.len()
8965                        )));
8966                    }
8967                    for (i, col) in cols.into_iter().enumerate() {
8968                        let mut sub = inner.clone();
8969                        sub.items = alloc::vec![sub.items[i].clone()];
8970                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8971                    }
8972                } else {
8973                    let mut exprs = alloc::vec![self.parse_expr(0)?];
8974                    while matches!(self.peek(), Token::Comma) {
8975                        self.advance();
8976                        exprs.push(self.parse_expr(0)?);
8977                    }
8978                    if !matches!(self.peek(), Token::RParen) {
8979                        return Err(self.err(format!(
8980                            "expected ')' after SET row values, got {:?}",
8981                            self.peek()
8982                        )));
8983                    }
8984                    self.advance();
8985                    if exprs.len() != cols.len() {
8986                        return Err(self.err(alloc::format!(
8987                            "SET (…) = (…) arity mismatch: {} columns, {} values",
8988                            cols.len(),
8989                            exprs.len()
8990                        )));
8991                    }
8992                    for (col, e) in cols.into_iter().zip(exprs) {
8993                        assignments.push((col, e));
8994                    }
8995                }
8996                if matches!(self.peek(), Token::Comma) {
8997                    self.advance();
8998                    continue;
8999                }
9000                break;
9001            }
9002            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9003            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9004            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9005            // `public.` dump qualifiers), so the qualifier has to be read off
9006            // the token stream first — otherwise `SET b.v = 888` would write
9007            // to the TARGET table's `v` while naming a source table, a
9008            // silent-wrong. A qualifier naming a SOURCE table means a
9009            // multi-TARGET update — mutating two tables in one statement —
9010            // which SPG does not model, so it is refused loudly.
9011            let set_qual: Option<String> = if mysql_from.is_some()
9012                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9013            {
9014                match self.peek() {
9015                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9016                    _ => None,
9017                }
9018            } else {
9019                None
9020            };
9021            let col = self.expect_ident_like()?;
9022            if let Some(q) = set_qual {
9023                let names_target = q.eq_ignore_ascii_case(&table)
9024                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9025                if !names_target {
9026                    return Err(self.err(alloc::format!(
9027                        "multi-table UPDATE can only assign to its first table \
9028                         ({table}); `{q}.{col}` targets another table"
9029                    )));
9030                }
9031            }
9032            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9033            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9034            // `__column_default` marker lowering just below). PG assigns to the
9035            // i-th (1-based) element, NULL-padding when i exceeds the length.
9036            if matches!(self.peek(), Token::LBracket) {
9037                self.advance();
9038                let index = self.parse_expr(0)?;
9039                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9040                // (and the open `arr[lo:]`), lowered to
9041                // `__array_assign_slice`. Only the single-subscript form
9042                // parsed before, so a slice assignment was a syntax error.
9043                let mut slice_hi: Option<Option<Expr>> = None;
9044                if matches!(self.peek(), Token::Colon) {
9045                    self.advance();
9046                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9047                        None
9048                    } else {
9049                        Some(self.parse_expr(0)?)
9050                    });
9051                }
9052                if !matches!(self.peek(), Token::RBracket) {
9053                    return Err(self.err(format!(
9054                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9055                        self.peek()
9056                    )));
9057                }
9058                self.advance();
9059                if !matches!(self.peek(), Token::Eq) {
9060                    return Err(self.err(format!(
9061                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9062                        self.peek()
9063                    )));
9064                }
9065                self.advance();
9066                let value = self.parse_expr(0)?;
9067                // PG merges several subscript writes to the same column into one
9068                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9069                // assignment to `col` rather than each overwriting the original.
9070                let existing = assignments.iter().position(|(c, _)| c == &col);
9071                let base = match existing {
9072                    Some(i) => assignments[i].1.clone(),
9073                    None => Expr::Column(ColumnName {
9074                        qualifier: None,
9075                        name: col.clone(),
9076                    }),
9077                };
9078                let assigned = match slice_hi {
9079                    None => Expr::FunctionCall {
9080                        name: "__array_assign".to_string(),
9081                        args: alloc::vec![base, index, value],
9082                    },
9083                    Some(hi) => Expr::FunctionCall {
9084                        name: "__array_assign_slice".to_string(),
9085                        args: alloc::vec![
9086                            base,
9087                            index,
9088                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9089                            value,
9090                        ],
9091                    },
9092                };
9093                match existing {
9094                    Some(i) => assignments[i].1 = assigned,
9095                    None => assignments.push((col, assigned)),
9096                }
9097                if matches!(self.peek(), Token::Comma) {
9098                    self.advance();
9099                    continue;
9100                }
9101                break;
9102            }
9103            if !matches!(self.peek(), Token::Eq) {
9104                return Err(self.err(format!(
9105                    "expected `=` after column name in UPDATE SET, got {:?}",
9106                    self.peek()
9107                )));
9108            }
9109            self.advance();
9110            // `SET col = DEFAULT` — the column's declared default;
9111            // rides out as a marker call the update executor
9112            // resolves against the schema.
9113            let value = if matches!(self.peek(), Token::Default) {
9114                self.advance();
9115                Expr::FunctionCall {
9116                    name: "__column_default".to_string(),
9117                    args: Vec::new(),
9118                }
9119            } else {
9120                self.parse_expr(0)?
9121            };
9122            assignments.push((col, value));
9123            if matches!(self.peek(), Token::Comma) {
9124                self.advance();
9125                continue;
9126            }
9127            break;
9128        }
9129        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9130        // update. Lowers onto the correlated-subquery machinery:
9131        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9132        // and each assignment that references a FROM-list table
9133        // wraps into a correlated scalar subquery
9134        // (SELECT expr FROM src WHERE cond). Equivalent for the
9135        // unique-join shape (the overwhelmingly common one); a
9136        // multi-match, which PG resolves by arbitrary pick,
9137        // surfaces as a scalar-subquery cardinality error instead
9138        // of a silent arbitrary result.
9139        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9140        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9141        // the SAME lowering below. Both spellings together is not legal in
9142        // either dialect.
9143        let from_clause = if let Some(fc) = mysql_from {
9144            if matches!(self.peek(), Token::From) {
9145                return Err(self.err(alloc::string::String::from(
9146                    "multi-table UPDATE already names its sources; drop the FROM clause",
9147                )));
9148            }
9149            Some(fc)
9150        } else if matches!(self.peek(), Token::From) {
9151            self.advance();
9152            Some(self.parse_from_clause()?)
9153        } else {
9154            None
9155        };
9156        let where_ = if matches!(self.peek(), Token::Where) {
9157            self.advance();
9158            Some(self.parse_expr(0)?)
9159        } else {
9160            None
9161        };
9162        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9163        // and the TARGET-row filter are NOT the same predicate once a LEFT
9164        // join is involved:
9165        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9166        //     one conjunction, and the whole thing filters target rows via
9167        //     EXISTS.
9168        //   * LEFT join: only the ON predicate belongs inside the source
9169        //     subquery. The WHERE still filters TARGET rows (with source
9170        //     columns read through the correlated subquery, which yields NULL
9171        //     for an unmatched row — exactly LEFT-join semantics).
9172        // Round 420 folded ON into WHERE unconditionally and then dropped the
9173        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9174        // WHERE a.id > 1` updated EVERY row.
9175        let sub_where = match (mysql_on.clone(), where_.clone()) {
9176            _ if mysql_outer => mysql_on.clone(),
9177            (Some(on), Some(w)) => Some(Expr::Binary {
9178                lhs: Box::new(on),
9179                op: crate::ast::BinOp::And,
9180                rhs: Box::new(w),
9181            }),
9182            (Some(on), None) => Some(on),
9183            (None, w) => w,
9184        };
9185        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9186        // has no such clause on UPDATE, so this is accepted only under the
9187        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9188        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9189        let mut returning = self.parse_optional_returning()?;
9190        // v7.39 (round 533) — kept for the engine, which can resolve the
9191        // UNQUALIFIED leaves this lowering has to leave alone.
9192        let from_sources = from_clause.as_ref().map(|fc| {
9193            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9194                from: fc.clone(),
9195                sub_where: sub_where.clone(),
9196            })
9197        });
9198        let (assignments, where_) = if let Some(fc) = from_clause {
9199            let names: Vec<String> = core::iter::once(&fc.primary)
9200                .chain(fc.joins.iter().map(|j| &j.table))
9201                .flat_map(|t| {
9202                    t.alias
9203                        .clone()
9204                        .into_iter()
9205                        .chain(core::iter::once(t.name.clone()))
9206                })
9207                .collect();
9208            let refs_list = |e: &Expr| -> bool {
9209                fn walk(e: &Expr, names: &[String]) -> bool {
9210                    match e {
9211                        Expr::Column(c) => c
9212                            .qualifier
9213                            .as_deref()
9214                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9215                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9216                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9217                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9218                        Expr::Case {
9219                            operand,
9220                            branches,
9221                            else_branch,
9222                        } => {
9223                            operand.as_deref().is_some_and(|o| walk(o, names))
9224                                || branches
9225                                    .iter()
9226                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9227                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9228                        }
9229                        _ => false,
9230                    }
9231                }
9232                walk(e, &names)
9233            };
9234            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9235                locking: None,
9236                ctes: Vec::new(),
9237                distinct: false,
9238                distinct_on: Vec::new(),
9239                items,
9240                from: Some(fc.clone()),
9241                where_: sub_where.clone(),
9242                group_by: None,
9243                group_by_all: false,
9244                having: None,
9245                unions: Vec::new(),
9246                order_by: Vec::new(),
9247                limit: None,
9248                offset: None,
9249                limit_with_ties: false,
9250                window_check_exprs: Vec::new(),
9251            };
9252            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9253            // assignment RHS with a correlated scalar subquery, instead of
9254            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9255            // column reference (`SET v = v + u.bonus`, where `v` is the target
9256            // table's column) inside a subquery whose FROM only has the source
9257            // table, so the unqualified `v` resolved against the source and
9258            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9259            // context — where they belong — fixes it; only the source columns
9260            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9261            // compound variants the leaf-walk doesn't decompose.
9262            let make_subq = |inner: Expr| {
9263                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9264                    expr: inner,
9265                    alias: None,
9266                }])))
9267            };
9268            let assignments = assignments
9269                .into_iter()
9270                .map(|(col, mut expr)| {
9271                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9272                    (col, expr)
9273                })
9274                .collect();
9275            let exists = Expr::Exists {
9276                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9277                    expr: Expr::Literal(Literal::Integer(1)),
9278                    alias: None,
9279                }])),
9280                negated: false,
9281            };
9282            // v7.39 (round 241) — RETURNING may reference the FROM-list
9283            // tables too (`RETURNING emp.id, dept.name`); the same
9284            // leaf-to-correlated-subquery lowering the assignments get.
9285            // Without it the qualifier died at eval with "unknown table
9286            // qualifier". (RETURNING was parsed before this block — the
9287            // lowering is a pure AST transformation.)
9288            if let Some(items) = returning.as_mut() {
9289                for item in items.iter_mut() {
9290                    if let SelectItem::Expr { expr, .. } = item {
9291                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9292                    }
9293                }
9294            }
9295            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9296            // EVERY matching target row: it gets no EXISTS filter, but the
9297            // caller's WHERE still applies, with source columns read through
9298            // the correlated subquery (NULL when unmatched — LEFT-join
9299            // semantics). `sub_where` above already excluded the WHERE from
9300            // the source subquery for this case.
9301            if mysql_outer {
9302                let mut outer = where_;
9303                if let Some(w) = outer.as_mut() {
9304                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9305                }
9306                (assignments, outer)
9307            } else {
9308                (assignments, Some(exists))
9309            }
9310        } else {
9311            (assignments, where_)
9312        };
9313        Ok(Statement::Update(crate::ast::UpdateStatement {
9314            ctes: Vec::new(),
9315            table,
9316            only,
9317            alias,
9318            assignments,
9319            from_sources,
9320            where_,
9321            order_limit: update_order_limit,
9322            returning,
9323        }))
9324    }
9325
9326    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9327    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9328    /// clause and its meaning are identical, so both call this rather than
9329    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9330    /// legal. PG has no such clause on either statement, so it is read only
9331    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9332    /// errors.
9333    ///
9334    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9335    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9336    /// stack in round 430.
9337    #[inline(never)]
9338    fn parse_mysql_dml_order_limit(
9339        &mut self,
9340        what: &str,
9341    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9342        if !self.mysql_dialect {
9343            return Ok(None);
9344        }
9345        let order_by = self.parse_order_by_keys()?;
9346        let limit = if matches!(self.peek(), Token::Limit) {
9347            self.advance();
9348            let tok = self.advance();
9349            let Token::Integer(n) = tok else {
9350                return Err(self.err(alloc::format!(
9351                    "expected integer after {what} LIMIT, got {tok:?}"
9352                )));
9353            };
9354            // MySQL rejects the `LIMIT offset, count` form here — only a
9355            // single row count is legal on a DML statement.
9356            if matches!(self.peek(), Token::Comma) {
9357                return Err(self.err(alloc::format!(
9358                    "{what} LIMIT takes a row count, not an offset"
9359                )));
9360            }
9361            let n = u32::try_from(n)
9362                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9363            Some(n)
9364        } else {
9365            None
9366        };
9367        if order_by.is_empty() && limit.is_none() {
9368            return Ok(None);
9369        }
9370        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9371            order_by,
9372            limit,
9373        })))
9374    }
9375
9376    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9377    /// the leading `DELETE` ident.
9378    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9379        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9380        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9381        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9382        // parse here; it reaches the existing USING path with the target
9383        // repeated in the list, which the source-list peel below handles.)
9384        // More than one name is a multi-TARGET delete, which SPG does not
9385        // model; it is refused rather than half-applied.
9386        let mysql_pre_target: Option<String> =
9387            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9388                let first = self.expect_ident_like()?;
9389                if matches!(self.peek(), Token::Comma) {
9390                    return Err(self.err(alloc::format!(
9391                        "multi-table DELETE can only delete from one table; \
9392                     `DELETE {first}, …` names several"
9393                    )));
9394                }
9395                Some(first)
9396            } else {
9397                None
9398            };
9399        if !matches!(self.peek(), Token::From) {
9400            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9401        }
9402        self.advance();
9403        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9404        // lookahead as the UPDATE spelling.
9405        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9406            if s.eq_ignore_ascii_case("only"))
9407            && matches!(
9408                self.tokens.get(self.pos + 1),
9409                Some(Token::Ident(_) | Token::QuotedIdent(_))
9410            );
9411        if only {
9412            self.advance();
9413        }
9414        let table = self.expect_ident_like()?;
9415        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9416        // spelling must not swallow the clause keywords that can follow
9417        // the target.
9418        let alias = if matches!(self.peek(), Token::As) {
9419            self.advance();
9420            Some(self.expect_ident_like()?)
9421        } else {
9422            match self.peek() {
9423                Token::Ident(s) | Token::QuotedIdent(s)
9424                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9425                {
9426                    let a = s.clone();
9427                    self.advance();
9428                    Some(a)
9429                }
9430                _ => None,
9431            }
9432        };
9433        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9434        // through the SAME join grammar the FROM clause uses (see the
9435        // `advance()`-destroys-tokens note on `parse_from_joins`).
9436        let mut mysql_on: Option<Expr> = None;
9437        let mut mysql_outer = false;
9438        let mysql_using = if mysql_pre_target.is_some()
9439            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9440        {
9441            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9442            let mut joins = self.parse_from_joins(&target_qual)?;
9443            if joins.is_empty() {
9444                return Err(self.err(alloc::string::String::from(
9445                    "multi-table DELETE needs at least one source table",
9446                )));
9447            }
9448            let head = joins.remove(0);
9449            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9450            mysql_on = head.on;
9451            Some(FromClause {
9452                primary: head.table,
9453                joins,
9454            })
9455        } else {
9456            None
9457        };
9458        // The pre-FROM target must be the table the FROM names (or its
9459        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9460        // is not the scan target.
9461        if let Some(t) = &mysql_pre_target {
9462            let names_target = t.eq_ignore_ascii_case(&table)
9463                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9464            if !names_target {
9465                return Err(self.err(alloc::format!(
9466                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9467                )));
9468            }
9469        }
9470        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9471        // delete. Same lowering as UPDATE … FROM: the WHERE
9472        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9473        // target row by the correlated machinery.
9474        let using_clause = if let Some(fc) = mysql_using {
9475            Some(fc)
9476        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9477            self.advance();
9478            let mut fc = self.parse_from_clause()?;
9479            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9480            // repeats the TARGET as the first USING entry (PG's spelling
9481            // lists only the extra sources). Peel it so the source subquery
9482            // does not re-scan — and shadow — the target table.
9483            let primary_is_target =
9484                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9485            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9486                let head = fc.joins.remove(0);
9487                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9488                mysql_on = head.on;
9489                fc = FromClause {
9490                    primary: head.table,
9491                    joins: fc.joins,
9492                };
9493            }
9494            Some(fc)
9495        } else {
9496            None
9497        };
9498        let where_ = if matches!(self.peek(), Token::Where) {
9499            self.advance();
9500            Some(self.parse_expr(0)?)
9501        } else {
9502            None
9503        };
9504        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9505        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9506        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9507        let mut returning = self.parse_optional_returning()?;
9508        let where_ = if let Some(fc) = using_clause {
9509            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9510            // a USING-table reference in RETURNING becomes a correlated
9511            // scalar subquery over the USING list.
9512            let names: Vec<String> = core::iter::once(&fc.primary)
9513                .chain(fc.joins.iter().map(|j| &j.table))
9514                .flat_map(|t| {
9515                    t.alias
9516                        .clone()
9517                        .into_iter()
9518                        .chain(core::iter::once(t.name.clone()))
9519                })
9520                .collect();
9521            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9522            // join filters the SOURCE subquery on the ON predicate alone and
9523            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9524            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9525            // rows); every other form folds ON and WHERE into one EXISTS.
9526            let sub_where = match (mysql_on.clone(), where_.clone()) {
9527                _ if mysql_outer => mysql_on.clone(),
9528                (Some(on), Some(w)) => Some(Expr::Binary {
9529                    lhs: Box::new(on),
9530                    op: crate::ast::BinOp::And,
9531                    rhs: Box::new(w),
9532                }),
9533                (Some(on), None) => Some(on),
9534                (None, w) => w,
9535            };
9536            let exists_where = sub_where.clone();
9537            let sub_fc = fc.clone();
9538            let make_subq = move |leaf: Expr| -> Expr {
9539                Expr::ScalarSubquery(Box::new(SelectStatement {
9540                    locking: None,
9541                    ctes: Vec::new(),
9542                    distinct: false,
9543                    distinct_on: Vec::new(),
9544                    items: alloc::vec![SelectItem::Expr {
9545                        expr: leaf,
9546                        alias: None,
9547                    }],
9548                    from: Some(sub_fc.clone()),
9549                    where_: sub_where.clone(),
9550                    group_by: None,
9551                    group_by_all: false,
9552                    having: None,
9553                    unions: Vec::new(),
9554                    order_by: Vec::new(),
9555                    limit: None,
9556                    offset: None,
9557                    limit_with_ties: false,
9558                    window_check_exprs: Vec::new(),
9559                }))
9560            };
9561            let refs = |e: &Expr| expr_refs_tables(e, &names);
9562            if let Some(items) = returning.as_mut() {
9563                for item in items.iter_mut() {
9564                    if let SelectItem::Expr { expr, .. } = item {
9565                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9566                    }
9567                }
9568            }
9569            // A LEFT join deletes the target rows the WHERE selects, reading
9570            // source columns through the correlated subquery (NULL when
9571            // unmatched); no EXISTS row filter.
9572            if mysql_outer {
9573                let mut outer = where_;
9574                if let Some(w) = outer.as_mut() {
9575                    wrap_from_leaves(w, &names, &make_subq, &refs);
9576                }
9577                outer
9578            } else {
9579                Some(Expr::Exists {
9580                    subquery: Box::new(SelectStatement {
9581                        locking: None,
9582                        ctes: Vec::new(),
9583                        distinct: false,
9584                        distinct_on: Vec::new(),
9585                        items: alloc::vec![SelectItem::Expr {
9586                            expr: Expr::Literal(Literal::Integer(1)),
9587                            alias: None,
9588                        }],
9589                        from: Some(fc),
9590                        where_: exists_where,
9591                        group_by: None,
9592                        group_by_all: false,
9593                        having: None,
9594                        unions: Vec::new(),
9595                        order_by: Vec::new(),
9596                        limit: None,
9597                        offset: None,
9598                        limit_with_ties: false,
9599                        window_check_exprs: Vec::new(),
9600                    }),
9601                    negated: false,
9602                })
9603            }
9604        } else {
9605            where_
9606        };
9607        Ok(Statement::Delete(crate::ast::DeleteStatement {
9608            ctes: Vec::new(),
9609            table,
9610            only,
9611            alias,
9612            where_,
9613            order_limit: delete_order_limit,
9614            returning,
9615        }))
9616    }
9617
9618    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9619    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9620    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9621    /// keyword. v7.17 surface:
9622    ///   * source: table reference (subquery source is a follow-up)
9623    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9624    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9625    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9626    ///     order
9627    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9628        // INTO
9629        let is_into_kw = matches!(self.peek(), Token::Into)
9630            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9631        if !is_into_kw {
9632            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9633        }
9634        self.advance();
9635        let target = self.expect_ident_like()?;
9636        // Optional alias — bare ident before USING.
9637        let target_alias = match self.peek() {
9638            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9639                Some(self.expect_ident_like()?)
9640            }
9641            _ => None,
9642        };
9643        // USING
9644        let is_using_kw = matches!(
9645            self.peek(),
9646            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9647        );
9648        if !is_using_kw {
9649            return Err(self.err(format!(
9650                "expected USING after MERGE INTO target, got {:?}",
9651                self.peek()
9652            )));
9653        }
9654        self.advance();
9655        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9656        // <table> [alias]`. PG requires an alias after a subquery source.
9657        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9658            self.advance(); // (
9659            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9660            // constant-SELECT lowering the derived-table parser uses
9661            // (PG deletes through this form; it was a parse error).
9662            let inner = if matches!(self.peek(), Token::Values) {
9663                self.advance(); // VALUES
9664                Statement::Select(self.parse_values_rows_body()?)
9665            } else {
9666                self.parse_select_stmt()?
9667            };
9668            match self.advance() {
9669                Token::RParen => {}
9670                other => {
9671                    return Err(self.err(format!(
9672                        "expected ')' after MERGE USING subquery, got {other:?}"
9673                    )));
9674                }
9675            }
9676            let Statement::Select(sub) = inner else {
9677                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9678            };
9679            (String::new(), Some(Box::new(sub)))
9680        } else {
9681            (self.expect_ident_like()?, None)
9682        };
9683        let source_alias = match self.peek() {
9684            Token::Ident(s) | Token::QuotedIdent(s)
9685                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9686            {
9687                Some(self.expect_ident_like()?)
9688            }
9689            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9690                self.advance(); // AS
9691                Some(self.expect_ident_like()?)
9692            }
9693            _ => None,
9694        };
9695        // v7.39 (round 768, F31-D5) — optional positional column-alias
9696        // list after the source alias (`s(id, v)`).
9697        let mut source_column_aliases: Vec<String> = Vec::new();
9698        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9699            self.advance();
9700            loop {
9701                source_column_aliases.push(self.expect_ident_like()?);
9702                match self.peek() {
9703                    Token::Comma => {
9704                        self.advance();
9705                    }
9706                    Token::RParen => {
9707                        self.advance();
9708                        break;
9709                    }
9710                    other => {
9711                        return Err(self.err(format!(
9712                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9713                        )));
9714                    }
9715                }
9716            }
9717        }
9718        if source_select.is_some() && source_alias.is_none() {
9719            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9720        }
9721        // ON
9722        if !matches!(self.peek(), Token::On) {
9723            return Err(self.err(format!(
9724                "expected ON after MERGE … USING source, got {:?}",
9725                self.peek()
9726            )));
9727        }
9728        self.advance();
9729        let on = self.parse_expr(0)?;
9730        // One or more WHEN clauses.
9731        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9732        loop {
9733            let is_when_kw = matches!(
9734                self.peek(),
9735                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9736            );
9737            if !is_when_kw {
9738                break;
9739            }
9740            self.advance(); // WHEN
9741            // [NOT] MATCHED
9742            let matched = if matches!(self.peek(), Token::Not) {
9743                self.advance();
9744                crate::ast::MergeMatched::NotMatched
9745            } else {
9746                crate::ast::MergeMatched::Matched
9747            };
9748            let is_matched_kw = matches!(
9749                self.peek(),
9750                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9751            );
9752            if !is_matched_kw {
9753                return Err(self.err(format!(
9754                    "expected MATCHED in WHEN clause, got {:?}",
9755                    self.peek()
9756                )));
9757            }
9758            self.advance();
9759            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9760            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9761            // to fire for target rows no source row matches.
9762            let mut matched = matched;
9763            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9764                self.advance();
9765                match self.peek() {
9766                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9767                        self.advance();
9768                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9769                    }
9770                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9771                        self.advance();
9772                    }
9773                    other => {
9774                        return Err(self.err(format!(
9775                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9776                        )));
9777                    }
9778                }
9779            }
9780            // Optional AND <expr>
9781            let condition = if matches!(self.peek(), Token::And) {
9782                self.advance();
9783                Some(self.parse_expr(0)?)
9784            } else {
9785                None
9786            };
9787            // THEN
9788            let is_then_kw = matches!(
9789                self.peek(),
9790                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9791            );
9792            if !is_then_kw {
9793                return Err(self.err(format!(
9794                    "expected THEN in WHEN clause, got {:?}",
9795                    self.peek()
9796                )));
9797            }
9798            self.advance();
9799            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9800            let action = match self.peek().clone() {
9801                Token::Insert => {
9802                    self.advance();
9803                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9804                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9805                    // VALUES (…)` omits it and fills every column in declaration
9806                    // order. PG accepts this; SPG used to require the list.
9807                    let mut columns: Vec<String> = Vec::new();
9808                    if matches!(self.peek(), Token::LParen) {
9809                        self.advance();
9810                        loop {
9811                            columns.push(self.expect_ident_like()?);
9812                            if matches!(self.peek(), Token::Comma) {
9813                                self.advance();
9814                                continue;
9815                            }
9816                            break;
9817                        }
9818                        if !matches!(self.peek(), Token::RParen) {
9819                            return Err(self.err(format!(
9820                                "expected ')' after INSERT column list, got {:?}",
9821                                self.peek()
9822                            )));
9823                        }
9824                        self.advance();
9825                    }
9826                    // VALUES (...)
9827                    if !matches!(self.peek(), Token::Values) {
9828                        return Err(self.err(format!(
9829                            "expected VALUES in MERGE INSERT, got {:?}",
9830                            self.peek()
9831                        )));
9832                    }
9833                    self.advance();
9834                    if !matches!(self.peek(), Token::LParen) {
9835                        return Err(self.err(format!(
9836                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
9837                            self.peek()
9838                        )));
9839                    }
9840                    self.advance();
9841                    let mut values: Vec<crate::ast::Expr> = Vec::new();
9842                    loop {
9843                        values.push(self.parse_expr(0)?);
9844                        if matches!(self.peek(), Token::Comma) {
9845                            self.advance();
9846                            continue;
9847                        }
9848                        break;
9849                    }
9850                    if !matches!(self.peek(), Token::RParen) {
9851                        return Err(self.err(format!(
9852                            "expected ')' after MERGE INSERT values, got {:?}",
9853                            self.peek()
9854                        )));
9855                    }
9856                    self.advance();
9857                    // Empty column list = positional into every column, so the
9858                    // count is checked against the table arity at execution.
9859                    if !columns.is_empty() && columns.len() != values.len() {
9860                        return Err(self.err(format!(
9861                            "MERGE INSERT column count ({}) ≠ value count ({})",
9862                            columns.len(),
9863                            values.len()
9864                        )));
9865                    }
9866                    crate::ast::MergeAction::Insert { columns, values }
9867                }
9868                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9869                    self.advance();
9870                    // SET
9871                    let is_set_kw = matches!(
9872                        self.peek(),
9873                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9874                    );
9875                    if !is_set_kw {
9876                        return Err(self.err(format!(
9877                            "expected SET after UPDATE in MERGE, got {:?}",
9878                            self.peek()
9879                        )));
9880                    }
9881                    self.advance();
9882                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9883                    loop {
9884                        let col = self.expect_ident_like()?;
9885                        if !matches!(self.peek(), Token::Eq) {
9886                            return Err(self.err(format!(
9887                                "expected '=' in MERGE UPDATE assignment, got {:?}",
9888                                self.peek()
9889                            )));
9890                        }
9891                        self.advance();
9892                        let expr = self.parse_expr(0)?;
9893                        assignments.push((col, expr));
9894                        if matches!(self.peek(), Token::Comma) {
9895                            self.advance();
9896                            continue;
9897                        }
9898                        break;
9899                    }
9900                    crate::ast::MergeAction::Update { assignments }
9901                }
9902                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9903                    self.advance();
9904                    crate::ast::MergeAction::Delete
9905                }
9906                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9907                    self.advance();
9908                    let is_nothing_kw = matches!(
9909                        self.peek(),
9910                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9911                    );
9912                    if !is_nothing_kw {
9913                        return Err(self.err(format!(
9914                            "expected NOTHING after DO in MERGE clause, got {:?}",
9915                            self.peek()
9916                        )));
9917                    }
9918                    self.advance();
9919                    crate::ast::MergeAction::DoNothing
9920                }
9921                other => {
9922                    return Err(self.err(format!(
9923                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9924                    )));
9925                }
9926            };
9927            // PG's grammar simply has no INSERT production under BY SOURCE
9928            // (a target row already exists there) — same syntax error.
9929            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9930                && matches!(action, crate::ast::MergeAction::Insert { .. })
9931            {
9932                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9933            }
9934            clauses.push(crate::ast::MergeWhenClause {
9935                matched,
9936                condition,
9937                action,
9938            });
9939        }
9940        if clauses.is_empty() {
9941            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9942        }
9943        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9944        // unconditional (no `AND`) WHEN of the same match kind: it could
9945        // never fire. Check per match kind in clause order.
9946        let mut seen_unconditional_matched = false;
9947        let mut seen_unconditional_not_matched = false;
9948        let mut seen_unconditional_by_source = false;
9949        for c in &clauses {
9950            let seen = match c.matched {
9951                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9952                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9953                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9954            };
9955            if *seen {
9956                return Err(self.err(String::from(
9957                    "unreachable WHEN clause specified after unconditional WHEN clause",
9958                )));
9959            }
9960            if c.condition.is_none() {
9961                *seen = true;
9962            }
9963        }
9964        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9965        let returning = self.parse_optional_returning()?;
9966        Ok(Statement::Merge(crate::ast::MergeStatement {
9967            // Attached by `parse_with_cte_then_select` when the MERGE
9968            // heads a WITH clause (round 149).
9969            ctes: Vec::new(),
9970            target,
9971            target_alias,
9972            source,
9973            source_alias,
9974            source_select,
9975            source_column_aliases,
9976            on,
9977            clauses,
9978            returning,
9979        }))
9980    }
9981
9982    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9983    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9984    /// as SELECT, so `RETURNING *`, `RETURNING col`,
9985    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9986    fn parse_optional_returning(
9987        &mut self,
9988    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
9989        let is_returning_kw = matches!(
9990            self.peek(),
9991            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
9992        );
9993        if !is_returning_kw {
9994            return Ok(None);
9995        }
9996        self.advance();
9997        let mut items = Vec::new();
9998        loop {
9999            items.push(self.parse_select_item()?);
10000            if matches!(self.peek(), Token::Comma) {
10001                self.advance();
10002                continue;
10003            }
10004            break;
10005        }
10006        Ok(Some(items))
10007    }
10008
10009    /// v6.0.4 — parse the tail of an ALTER statement after the
10010    /// leading `ALTER` keyword has been consumed. Only one form is
10011    /// supported in v6.0.4:
10012    ///
10013    /// ```text
10014    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10015    /// ```
10016    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10017        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10018        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10019        // exclusion) is accepted by stripping the `ONLY` keyword
10020        // before the table parse.
10021        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10022        // and the long PG-dump tail are accepted as no-ops.
10023        match self.advance() {
10024            Token::Index => {}
10025            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10026            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10027            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10028            Token::Table => {
10029                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10030                    self.advance();
10031                }
10032                return self.parse_alter_table_after_keyword();
10033            }
10034            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10035                return self.parse_alter_policy_after_keyword();
10036            }
10037            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10038                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10039                    self.advance();
10040                }
10041                return self.parse_alter_table_after_keyword();
10042            }
10043            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10044            // of the silent-noop tail.
10045            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10046                return self.parse_alter_sequence_after_keyword();
10047            }
10048            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10049            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10050            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10051            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10052                // NB: the match arm consumed `TYPE` via self.advance(); the
10053                // cursor is now at the type name — do NOT advance again.
10054                let type_name = self.expect_ident_like()?;
10055                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10056                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10057                if is_add_value {
10058                    self.advance(); // ADD
10059                    self.advance(); // VALUE
10060                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10061                    // IF/EXISTS as identifiers.
10062                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10063                    {
10064                        let n1 = self.tokens.get(self.pos + 1);
10065                        let n2 = self.tokens.get(self.pos + 2);
10066                        if matches!(n1, Some(Token::Not))
10067                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10068                        {
10069                            self.advance();
10070                            self.advance();
10071                            self.advance();
10072                            true
10073                        } else {
10074                            false
10075                        }
10076                    } else {
10077                        false
10078                    };
10079                    let label = self.expect_string_literal()?;
10080                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10081                    {
10082                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10083                        self.advance();
10084                        let anchor = self.expect_string_literal()?;
10085                        Some((is_before, anchor))
10086                    } else {
10087                        None
10088                    };
10089                    return Ok(Statement::AlterTypeAddValue {
10090                        type_name,
10091                        label,
10092                        if_not_exists,
10093                        position,
10094                    });
10095                }
10096                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10097                // Used to fall into the no-op tail below: accepted, silently
10098                // ignored. `RENAME TO <newtype>` keeps falling through.
10099                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10100                    && matches!(
10101                        self.tokens.get(self.pos + 1),
10102                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10103                    )
10104                {
10105                    self.advance(); // RENAME
10106                    self.advance(); // VALUE
10107                    let old = self.expect_string_literal()?;
10108                    if matches!(self.peek(), Token::To) {
10109                        self.advance();
10110                    } else {
10111                        self.expect_keyword_ident("to")?;
10112                    }
10113                    let new = self.expect_string_literal()?;
10114                    return Ok(Statement::AlterTypeRenameValue {
10115                        type_name,
10116                        old,
10117                        new,
10118                    });
10119                }
10120                // Other ALTER TYPE forms — the ACTION stays a no-op
10121                // (pg_dump tail), but v7.39 (round 708) the NAME is
10122                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10123                // success for a type that does not exist.
10124                self.consume_until_statement_boundary();
10125                return Ok(Statement::ValidateOnly {
10126                    kind: crate::ast::ValidateOnlyKind::TypeName,
10127                    names: alloc::vec![type_name],
10128                });
10129            }
10130            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10131            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10132            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10133            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10134            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10135            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10136            // pg_dump no-op list below: every form used to report success
10137            // and change nothing, which is worse than refusing outright
10138            // (a migration dropping a constraint kept rejecting data).
10139            // NOTE: the enclosing `match self.advance()` already consumed
10140            // the DOMAIN keyword, so the name is next.
10141            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10142                return self.parse_alter_domain_after_keyword();
10143            }
10144            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10145            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10146            // used to fall into the pg_dump no-op tail below, so a DBA
10147            // setting a per-role default was told it worked and nothing
10148            // happened. Intercepted here, BEFORE that tail.
10149            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10150            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10151            // interception below exists: swallowed with the no-op tail, an
10152            // unknown parameter name was ACCEPTED where PG18 answers
10153            // `unrecognized configuration parameter`. SPG applies nothing
10154            // either way — there is no postgresql.auto.conf — but it now
10155            // says so about a name it does not know.
10156            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10157                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10158                // already consumed here. An extra advance eats the SET and
10159                // the parameter name is never seen — which is exactly the
10160                // bug a panic in this branch disproved: the branch WAS on
10161                // the path, the reading of it was wrong.
10162                let mut parameter = None;
10163                // SET <name> … | RESET <name> | RESET ALL
10164                if matches!(self.peek(), Token::Ident(k)
10165                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10166                {
10167                    self.advance();
10168                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10169                        && !n.eq_ignore_ascii_case("all")
10170                    {
10171                        self.advance();
10172                        // A dotted GUC (`plpgsql.check_asserts`) is two
10173                        // tokens; keep the whole name.
10174                        let mut full = n;
10175                        while matches!(self.peek(), Token::Dot) {
10176                            self.advance();
10177                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10178                                full.push('.');
10179                                full.push_str(&t);
10180                            }
10181                        }
10182                        parameter = Some(full);
10183                    }
10184                }
10185                self.consume_until_statement_boundary();
10186                return Ok(Statement::AlterSystem { parameter });
10187            }
10188            Token::Ident(s) | Token::QuotedIdent(s)
10189                if matches!(
10190                    s.to_ascii_lowercase().as_str(),
10191                    "role" | "user" | "database"
10192                ) && self.peeks_db_role_setting() =>
10193            {
10194                let is_database = s.eq_ignore_ascii_case("database");
10195                return self.parse_db_role_setting(is_database);
10196            }
10197            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10198            // (the non-SET forms; SET/RESET took the branch above). The
10199            // attributes still no-op — recorded, and the ignored PASSWORD
10200            // is ledgered as its own follow-up — but the ROLE is validated:
10201            // any name was accepted for a role that does not exist.
10202            Token::Ident(s) | Token::QuotedIdent(s)
10203                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10204            {
10205                // NB: the enclosing `match self.advance()` already consumed
10206                // ROLE/USER — the round-695 trap, hit again in this round's
10207                // first draft (the name was eaten and WITH parsed as the
10208                // role). The cursor is at the name.
10209                let name = self.expect_ident_or_string()?;
10210                // v7.39 (round 750) — scan the attribute tail for
10211                // PASSWORD. Everything else stays a recorded no-op, but
10212                // a dropped credential rotation is a SECURITY bug:
10213                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10214                // changed nothing, so the old password kept working.
10215                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10216                // NULL` clears the credential.
10217                let mut password: Option<Option<String>> = None;
10218                loop {
10219                    match self.peek() {
10220                        Token::Semicolon | Token::Eof => break,
10221                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10222                            self.advance();
10223                            match self.advance() {
10224                                Token::String(p) => password = Some(Some(p)),
10225                                Token::Null => password = Some(None),
10226                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10227                                    password = Some(None);
10228                                }
10229                                other => {
10230                                    return Err(self.err(alloc::format!(
10231                                        "expected password string or NULL after PASSWORD, got {other:?}"
10232                                    )));
10233                                }
10234                            }
10235                        }
10236                        _ => {
10237                            self.advance();
10238                        }
10239                    }
10240                }
10241                if name.eq_ignore_ascii_case("all") {
10242                    // `ALTER ROLE ALL …` names every role; nothing to check.
10243                    return Ok(Statement::Empty);
10244                }
10245                if let Some(pw) = password {
10246                    return Ok(Statement::AlterRolePassword { name, password: pw });
10247                }
10248                return Ok(Statement::ValidateOnly {
10249                    kind: crate::ast::ValidateOnlyKind::RoleName,
10250                    names: alloc::vec![name],
10251                });
10252            }
10253            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10254            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10255            // list far enough to validate the NAME; the actions still no-op.
10256            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10257            // models none of them and their dumps are rare.)
10258            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10259                let name = self.expect_ident_or_string()?;
10260                self.consume_until_statement_boundary();
10261                return Ok(Statement::ValidateOnly {
10262                    kind: crate::ast::ValidateOnlyKind::CollationName,
10263                    names: alloc::vec![name],
10264                });
10265            }
10266            Token::Ident(s) | Token::QuotedIdent(s)
10267                if s.eq_ignore_ascii_case("text")
10268                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10269                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10270            {
10271                self.advance(); // SEARCH
10272                self.advance(); // CONFIGURATION
10273                let name = self.expect_ident_like()?;
10274                self.consume_until_statement_boundary();
10275                return Ok(Statement::ValidateOnly {
10276                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10277                    names: alloc::vec![name],
10278                });
10279            }
10280            Token::Ident(s) | Token::QuotedIdent(s)
10281                if s.eq_ignore_ascii_case("event")
10282                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10283            {
10284                self.advance(); // TRIGGER
10285                let name = self.expect_ident_like()?;
10286                self.consume_until_statement_boundary();
10287                return Ok(Statement::ValidateOnly {
10288                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10289                    names: alloc::vec![name],
10290                });
10291            }
10292            Token::Ident(s) | Token::QuotedIdent(s)
10293                if s.eq_ignore_ascii_case("large")
10294                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10295            {
10296                self.advance(); // OBJECT
10297                let oid = match self.advance() {
10298                    Token::Integer(n) => alloc::format!("{n}"),
10299                    other => {
10300                        return Err(
10301                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10302                        );
10303                    }
10304                };
10305                self.consume_until_statement_boundary();
10306                return Ok(Statement::ValidateOnly {
10307                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10308                    names: alloc::vec![oid],
10309                });
10310            }
10311            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10312            // argument-list parse as DROP AGGREGATE (round 707); the
10313            // action no-ops, the existence check is real.
10314            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10315                // Same round-695 trap as above: AGGREGATE is already
10316                // consumed; the cursor is at the name.
10317                let name = self.expect_ident_like()?;
10318                let mut names = alloc::vec![name];
10319                if matches!(self.peek(), Token::LParen) {
10320                    self.advance();
10321                    loop {
10322                        match self.peek().clone() {
10323                            Token::RParen => {
10324                                self.advance();
10325                                break;
10326                            }
10327                            Token::Star => {
10328                                self.advance();
10329                                names.push(String::from("*"));
10330                            }
10331                            Token::Comma => {
10332                                self.advance();
10333                            }
10334                            _ => {
10335                                let mut t = self.expect_ident_like()?;
10336                                while let Token::Ident(nx) = self.peek() {
10337                                    let nx = nx.clone();
10338                                    self.advance();
10339                                    t.push(' ');
10340                                    t.push_str(&nx);
10341                                }
10342                                names.push(t);
10343                            }
10344                        }
10345                    }
10346                }
10347                self.consume_until_statement_boundary();
10348                return Ok(Statement::ValidateOnly {
10349                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10350                    names,
10351                });
10352            }
10353            Token::Ident(s) | Token::QuotedIdent(s)
10354                if matches!(
10355                    s.to_ascii_lowercase().as_str(),
10356                    "view"
10357                        | "function"
10358                        | "database"
10359                        | "schema"
10360                        | "owner"
10361                        | "default"
10362                        | "extension"
10363                        | "materialized"
10364                        | "publication"
10365                        | "subscription"
10366                        // v7.37.17 (17.6 siblings) — additional ALTER
10367                        // targets pg_dump / pg_dumpall / operator DB
10368                        // migration scripts commonly emit. SPG has
10369                        // no matching machinery for any of these; the
10370                        // parser accepts + Empty-returns so pg_dump
10371                        // tail statements don't stall.
10372                        | "tablespace"
10373                        | "language"
10374                        | "operator"
10375                        | "conversion"
10376                        | "statistics"
10377                        | "server"
10378                        | "foreign"
10379                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10380                        // / TEMPLATE (CONFIGURATION intercepted above).
10381                        | "text"
10382                ) =>
10383            {
10384                self.consume_until_statement_boundary();
10385                return Ok(Statement::Empty);
10386            }
10387            other => {
10388                return Err(self.err(format!(
10389                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10390                     after ALTER, got {other:?}"
10391                )));
10392            }
10393        }
10394        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10395        // (mailrs migrate-042 ships these). The presence of an
10396        // IF EXISTS makes the subsequent name lookup tolerate
10397        // a missing index — engine returns CommandOk no-op.
10398        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10399            let next = self.tokens.get(self.pos + 1);
10400            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10401                self.advance();
10402                self.advance();
10403                true
10404            } else {
10405                false
10406            }
10407        } else {
10408            false
10409        };
10410        let name = self.expect_ident_like()?;
10411        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10412        // Detect BEFORE the REBUILD path so the existing REBUILD
10413        // arm stays untouched.
10414        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10415            self.advance();
10416            if matches!(self.peek(), Token::To) {
10417                self.advance();
10418            } else {
10419                self.expect_keyword_ident("to")?;
10420            }
10421            let new = self.expect_ident_like()?;
10422            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10423                name,
10424                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10425            }));
10426        }
10427        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10428        // A syntax error before; the index is validated, the params no-op.
10429        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10430            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10431                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10432        {
10433            self.consume_until_statement_boundary();
10434            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10435                name,
10436                target: crate::ast::AlterIndexTarget::StorageParams,
10437            }));
10438        }
10439        // REBUILD
10440        self.expect_keyword_ident("rebuild")?;
10441        // Optional: WITH (encoding = <enc>)
10442        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10443            self.advance();
10444            if !matches!(self.peek(), Token::LParen) {
10445                return Err(self.err(format!(
10446                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10447                    self.peek()
10448                )));
10449            }
10450            self.advance();
10451            self.expect_keyword_ident("encoding")?;
10452            if !matches!(self.peek(), Token::Eq) {
10453                return Err(self.err(format!(
10454                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10455                    self.peek()
10456                )));
10457            }
10458            self.advance();
10459            let enc_ident = match self.advance() {
10460                Token::Ident(s) | Token::QuotedIdent(s) => s,
10461                other => {
10462                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10463                }
10464            };
10465            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10466                "f32" => VecEncoding::F32,
10467                "sq8" => VecEncoding::Sq8,
10468                "half" => VecEncoding::F16,
10469                other => {
10470                    return Err(self.err(format!(
10471                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10472                    )));
10473                }
10474            };
10475            if !matches!(self.peek(), Token::RParen) {
10476                return Err(self.err(format!(
10477                    "expected ')' after encoding value, got {:?}",
10478                    self.peek()
10479                )));
10480            }
10481            self.advance();
10482            Some(enc)
10483        } else {
10484            None
10485        };
10486        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10487            name,
10488            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10489        }))
10490    }
10491
10492    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10493    /// only `SET` form currently supported; future v6.7.x can add
10494    /// more SET subjects without changing the dispatch shape.
10495    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10496    /// subactions. Single-subaction shape stays a 1-element vec.
10497    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10498        let table_name = self.expect_ident_like()?;
10499        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10500        loop {
10501            let subaction = self.parse_alter_table_subaction()?;
10502            // ADD COLUMN with inline REFERENCES emits both an
10503            // AddColumn and an AddForeignKey subaction; the
10504            // helper returns 1 or 2 items.
10505            targets.extend(subaction);
10506            if matches!(self.peek(), Token::Comma) {
10507                self.advance();
10508                continue;
10509            }
10510            break;
10511        }
10512        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10513            name: table_name,
10514            targets,
10515        }))
10516    }
10517
10518    /// Parse one ALTER TABLE subaction. Returns a Vec because
10519    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10520    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10521    fn parse_alter_table_subaction(
10522        &mut self,
10523    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10524        match self.peek() {
10525            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10526                self.advance();
10527                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10528                // storage parameters: paren-prefixed; consume.
10529                if matches!(self.peek(), Token::LParen) {
10530                    self.consume_until_statement_boundary();
10531                    return Ok(Vec::new());
10532                }
10533                let setting = self.expect_ident_like()?;
10534                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10535                    if !matches!(self.peek(), Token::Eq) {
10536                        return Err(self.err(alloc::format!(
10537                            "expected '=' after hot_tier_bytes, got {:?}",
10538                            self.peek()
10539                        )));
10540                    }
10541                    self.advance();
10542                    let n = self.expect_u64_literal()?;
10543                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10544                }
10545                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10546                // accept-and-no-op for ALTER TABLE SET <subject>
10547                // forms that pg_dump emits but SPG either treats
10548                // as N/A (single-tenant, single-owner, no shared
10549                // tablespaces) or accepts the dump-side declaration
10550                // without runtime effect:
10551                //   SET SCHEMA <name>            (18.11)
10552                //   SET TABLESPACE <name>        (18.8)
10553                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10554                //   SET WITHOUT CLUSTER          (18.13)
10555                //   SET WITHOUT OIDS             (PG legacy)
10556                //   SET (option = value, …)      (storage parameters)
10557                //   SET REPLICA IDENTITY {…}     (18.14)
10558                if setting.eq_ignore_ascii_case("schema")
10559                    || setting.eq_ignore_ascii_case("tablespace")
10560                    || setting.eq_ignore_ascii_case("logged")
10561                    || setting.eq_ignore_ascii_case("unlogged")
10562                    || setting.eq_ignore_ascii_case("without")
10563                {
10564                    self.consume_until_statement_boundary();
10565                    return Ok(Vec::new());
10566                }
10567                if setting.eq_ignore_ascii_case("replica") {
10568                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10569                    self.consume_until_statement_boundary();
10570                    return Ok(Vec::new());
10571                }
10572                // SET (option=value, …) — storage parameters.
10573                if matches!(self.peek(), Token::LParen) {
10574                    self.consume_until_statement_boundary();
10575                    return Ok(Vec::new());
10576                }
10577                Err(self.err(alloc::format!(
10578                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10579                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10580                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10581                )))
10582            }
10583            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10584            // not ignored: round 645 gave SPG the inheritance the
10585            // v7.37.18 no-op said it did not have.
10586            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10587                self.advance();
10588                let parent = self.expect_ident_like()?;
10589                self.consume_until_statement_boundary();
10590                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10591                    parent,
10592                    detach: false
10593                }])
10594            }
10595            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10596            // LEVEL SECURITY`, which has its own RLS arm below — without
10597            // the guard this swallowed NO FORCE as a no-op.
10598            Token::Ident(s)
10599                if s.eq_ignore_ascii_case("no")
10600                    && !matches!(
10601                        self.tokens.get(self.pos + 1),
10602                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10603                    ) =>
10604            {
10605                self.advance();
10606                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10607                    if k.eq_ignore_ascii_case("inherit"))
10608                {
10609                    self.advance();
10610                    let parent = self.expect_ident_like()?;
10611                    self.consume_until_statement_boundary();
10612                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10613                        parent,
10614                        detach: true
10615                    }]);
10616                }
10617                self.consume_until_statement_boundary();
10618                Ok(Vec::new())
10619            }
10620            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10621            // single-owner, so there is still nothing to record.
10622            //
10623            // v7.39 (round 652) — but the name now reaches the engine,
10624            // which refuses a role that does not exist as PG does. The
10625            // no-op was swallowing the whole statement, so a dump naming
10626            // a role this server never heard of restored clean and left
10627            // the table owned by whoever ran the restore.
10628            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10629                self.advance();
10630                if matches!(self.peek(), Token::To) {
10631                    self.advance();
10632                }
10633                let role = self.expect_ident_like()?;
10634                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10635                    role
10636                }])
10637            }
10638            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10639            // PG sets a hint; SPG doesn't have clustered storage, so the
10640            // hint itself stays a no-op.
10641            //
10642            // v7.39 (round 652) — the index name is checked now. PG
10643            // errors on one that does not exist, and swallowing that let
10644            // a typo'd CLUSTER ON pass silently.
10645            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10646                self.advance();
10647                // `ON` is a reserved token, not an ident.
10648                if !matches!(self.peek(), Token::On) {
10649                    return Err(self.err(alloc::format!(
10650                        "expected ON after CLUSTER, got {:?}",
10651                        self.peek()
10652                    )));
10653                }
10654                self.advance();
10655                let index = self.expect_ident_like()?;
10656                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10657                    index: Some(index)
10658                }])
10659            }
10660            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10661            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10662            // what a logical decoder puts in the old-tuple image; SPG's
10663            // replication is SQL-text, so there is nothing to record.
10664            // Accept-and-no-op (it used to be a parse error).
10665            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10666                self.advance();
10667                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10668                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10669                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10670                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10671                {
10672                    self.advance(); // IDENTITY
10673                    self.advance(); // USING
10674                    if matches!(self.peek(), Token::Index)
10675                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10676                    {
10677                        self.advance();
10678                    }
10679                    let index = self.expect_ident_like()?;
10680                    self.consume_until_statement_boundary();
10681                    return Ok(alloc::vec![
10682                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10683                    ]);
10684                }
10685                self.consume_until_statement_boundary();
10686                Ok(Vec::new())
10687            }
10688            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10689            //
10690            // v7.39 (round 652) — it used to consume the statement and
10691            // return nothing, on the stated theory that SPG validated at
10692            // ADD CONSTRAINT time so there was never anything left to
10693            // validate. Measured against PG18, ADD CONSTRAINT did not
10694            // scan the existing rows at all — the comment described a
10695            // property SPG did not have, which is why nobody looked. Both
10696            // halves are real now: ADD scans unless told NOT VALID, and
10697            // this scans what NOT VALID skipped.
10698            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10699                self.advance();
10700                self.expect_keyword_ident("constraint")?;
10701                let name = self.expect_ident_like()?;
10702                Ok(alloc::vec![
10703                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10704                ])
10705            }
10706            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10707            // SET (option = value, …). PG uses it to clear per-table
10708            // storage params like fillfactor or autovacuum_*. SPG
10709            // engine-manages those parameters; accept-and-no-op.
10710            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10711                self.advance();
10712                self.consume_until_statement_boundary();
10713                Ok(Vec::new())
10714            }
10715            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10716            // type-of binding (PG 9.0+). SPG composite types
10717            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10718            // TABLE OF is rare and inverse of CREATE TABLE OF.
10719            // Accept-and-no-op until a customer dump round-trips it.
10720            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10721                self.advance();
10722                // v7.39 (round 710) — the type name is validated now.
10723                let type_name = self.expect_ident_like()?;
10724                self.consume_until_statement_boundary();
10725                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10726                    type_name
10727                }])
10728            }
10729            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10730            // (reserved keyword) rather than Token::Ident("not"),
10731            // so it needs its own arm. Accept-and-no-op same as OF.
10732            Token::Not => {
10733                self.advance();
10734                self.consume_until_statement_boundary();
10735                Ok(Vec::new())
10736            }
10737            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10738            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10739                self.advance();
10740                self.expect_row_level_security()?;
10741                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10742                    enabled: None,
10743                    force: Some(true),
10744                }])
10745            }
10746            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10747            Token::Ident(s)
10748                if s.eq_ignore_ascii_case("no")
10749                    && matches!(
10750                        self.tokens.get(self.pos + 1),
10751                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10752                    ) =>
10753            {
10754                self.advance(); // NO
10755                self.advance(); // FORCE
10756                self.expect_row_level_security()?;
10757                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10758                    enabled: None,
10759                    force: Some(false),
10760                }])
10761            }
10762            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10763            // (sets relrowsecurity). The guard requires the next token to be
10764            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10765            Token::Ident(s)
10766                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10767                    && matches!(
10768                        self.tokens.get(self.pos + 1),
10769                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10770                    ) =>
10771            {
10772                let enabled = s.eq_ignore_ascii_case("enable");
10773                self.advance(); // ENABLE/DISABLE
10774                self.expect_row_level_security()?;
10775                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10776                    enabled: Some(enabled),
10777                    force: None,
10778                }])
10779            }
10780            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10781                self.advance();
10782                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10783                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10784                // emits. The same grammar CREATE TABLE already accepts
10785                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10786                // through the SAME parser — an ALTER-only copy would be a
10787                // second place for the two to drift.
10788                if self.peek_mysql_inline_key_start() {
10789                    return Ok(match self.parse_mysql_inline_key()? {
10790                        Some(c) => {
10791                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10792                        }
10793                        // FULLTEXT / SPATIAL parse and are accepted as a
10794                        // no-op here exactly as they are inline.
10795                        None => Vec::new(),
10796                    });
10797                }
10798                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10799                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10800                // PRIMARY KEY this way; mysqldump emits both.
10801                // Peek-only dispatch (no advance) — `advance()`
10802                // destructively replaces consumed tokens with Eof,
10803                // so saved-pos restore would land on Eofs.
10804                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10805                {
10806                    // The next-but-one ident is the constraint
10807                    // name; the one after THAT is the kind.
10808                    let kind_pos = self.pos + 2;
10809                    let kind = self.tokens.get(kind_pos).cloned();
10810                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10811                    {
10812                        let fk = self.parse_table_level_fk()?;
10813                        return Ok(alloc::vec![
10814                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10815                        ]);
10816                    }
10817                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10818                    {
10819                        self.advance(); // CONSTRAINT
10820                        // v7.39 (read01 round 48) — keep the name; the engine
10821                        // stores it now instead of dropping it on the floor.
10822                        let con_name = self.expect_ident_like()?;
10823                        self.advance(); // PRIMARY
10824                        self.expect_keyword_ident("key")?;
10825                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10826                        // v7.39 (round 711) — the ALTER form carries the
10827                        // timing too (pg_dump writes it here).
10828                        let (deferrable, initially_deferred) =
10829                            self.consume_deferrable_clauses_timed()?;
10830                        return Ok(alloc::vec![
10831                            crate::ast::AlterTableTarget::AddTableConstraint(
10832                                crate::ast::TableConstraint::PrimaryKey {
10833                                    name: Some(con_name),
10834                                    columns: cols,
10835                                    deferrable,
10836                                    initially_deferred,
10837                                }
10838                            )
10839                        ]);
10840                    }
10841                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10842                    {
10843                        self.advance(); // CONSTRAINT
10844                        // v7.39 (read01 round 48) — keep the name.
10845                        let con_name = self.expect_ident_like()?;
10846                        // v7.22 (mailrs round-13 gap 6) — delegate so
10847                        // the optional `NULLS [NOT] DISTINCT` modifier
10848                        // parses here too (pg_dump emits the ALTER
10849                        // form; semantics enforced by the engine
10850                        // since v7.13).
10851                        let mut uc = self.parse_table_level_unique()?;
10852                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10853                            *name = Some(con_name);
10854                        }
10855                        return Ok(alloc::vec![
10856                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10857                        ]);
10858                    }
10859                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10860                    {
10861                        self.advance(); // CONSTRAINT
10862                        // v7.39 (read01 round 48) — keep the name.
10863                        let con_name = self.expect_ident_like()?;
10864                        self.advance(); // CHECK
10865                        if !matches!(self.peek(), Token::LParen) {
10866                            return Err(self.err(alloc::format!(
10867                                "expected '(' after CHECK, got {:?}", self.peek()
10868                            )));
10869                        }
10870                        self.advance();
10871                        let expr = self.parse_expr(0)?;
10872                        if matches!(self.peek(), Token::RParen) {
10873                            self.advance();
10874                        }
10875                        let not_valid = self.parse_not_valid_suffix();
10876                        return Ok(alloc::vec![
10877                            crate::ast::AlterTableTarget::AddTableConstraint(
10878                                crate::ast::TableConstraint::Check {
10879                                    name: Some(con_name),
10880                                    expr,
10881                                    not_valid,
10882                                }
10883                            )
10884                        ]);
10885                    }
10886                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10887                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10888                    // exclusion constraints via this ALTER form.
10889                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10890                    {
10891                        self.advance(); // CONSTRAINT
10892                        let con_name = self.expect_ident_like()?;
10893                        let mut ex = self.parse_table_level_exclude()?;
10894                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10895                            *name = Some(con_name);
10896                        }
10897                        return Ok(alloc::vec![
10898                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10899                        ]);
10900                    }
10901                    // Unknown kind — fall through to FK path which
10902                    // produces a descriptive parse error.
10903                }
10904                let is_fk = matches!(
10905                    self.peek(),
10906                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10907                        || s.eq_ignore_ascii_case("foreign")
10908                );
10909                if is_fk {
10910                    let fk = self.parse_table_level_fk()?;
10911                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10912                }
10913                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10914                // (no CONSTRAINT prefix) — same dispatch.
10915                match self.peek().clone() {
10916                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10917                        self.advance();
10918                        self.expect_keyword_ident("key")?;
10919                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10920                        let (deferrable, initially_deferred) =
10921                            self.consume_deferrable_clauses_timed()?;
10922                        return Ok(alloc::vec![
10923                            crate::ast::AlterTableTarget::AddTableConstraint(
10924                                crate::ast::TableConstraint::PrimaryKey {
10925                                    name: None,
10926                                    columns: cols,
10927                                    deferrable,
10928                                    initially_deferred,
10929                                }
10930                            )
10931                        ]);
10932                    }
10933                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10934                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
10935                        let uc = self.parse_table_level_unique()?;
10936                        return Ok(alloc::vec![
10937                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10938                        ]);
10939                    }
10940                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10941                    // prefix). The other three bare forms were here and
10942                    // this one was not, so it fell through to the column
10943                    // path and came back as "unexpected reserved keyword
10944                    // 'check' at start of column definition".
10945                    _ if self.peek_table_level_check_start() => {
10946                        let chk = self.parse_table_level_check()?;
10947                        let not_valid = self.parse_not_valid_suffix();
10948                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10949                            unreachable!("parse_table_level_check returns Check")
10950                        };
10951                        return Ok(alloc::vec![
10952                            crate::ast::AlterTableTarget::AddTableConstraint(
10953                                crate::ast::TableConstraint::Check {
10954                                    name: None,
10955                                    expr,
10956                                    not_valid,
10957                                }
10958                            )
10959                        ]);
10960                    }
10961                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10962                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10963                        let ex = self.parse_table_level_exclude()?;
10964                        return Ok(alloc::vec![
10965                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10966                        ]);
10967                    }
10968                    _ => {}
10969                }
10970                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10971                    self.advance();
10972                }
10973                let mut if_not_exists = false;
10974                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10975                    self.advance();
10976                    if !matches!(self.peek(), Token::Not) {
10977                        return Err(self.err(alloc::format!(
10978                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10979                            self.peek()
10980                        )));
10981                    }
10982                    self.advance();
10983                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10984                        return Err(self.err(alloc::format!(
10985                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10986                            self.peek()
10987                        )));
10988                    }
10989                    self.advance();
10990                    if_not_exists = true;
10991                }
10992                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
10993                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
10994                // returns ColumnDef + an optional inline FK.
10995                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
10996                let col_name = column.name.clone();
10997                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
10998                    column,
10999                    if_not_exists,
11000                }];
11001                if let Some(mut fk) = col_level_fk {
11002                    if fk.columns.is_empty() {
11003                        fk.columns.push(col_name);
11004                    }
11005                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11006                }
11007                Ok(out)
11008            }
11009            Token::Drop => {
11010                self.advance();
11011                // v7.13.3 — dispatch on the next token. mailrs round-7
11012                // S8 closed DROP COLUMN; round-6 S7 closed
11013                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11014                // RESTRICT modifiers.
11015                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11016                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11017                let subject = match self.peek() {
11018                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11019                        self.advance();
11020                        "constraint"
11021                    }
11022                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11023                        self.advance();
11024                        "column"
11025                    }
11026                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11027                    // `INDEX` lexes as the reserved Token::Index, so it is
11028                    // unambiguous. `KEY` is a plain ident, and PG allows a
11029                    // column literally named "key", so only read it as the
11030                    // keyword when a name follows it.
11031                    Token::Index => {
11032                        self.advance();
11033                        "index"
11034                    }
11035                    Token::Ident(s)
11036                        if s.eq_ignore_ascii_case("key")
11037                            && matches!(
11038                                self.tokens.get(self.pos + 1),
11039                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11040                            ) =>
11041                    {
11042                        self.advance();
11043                        "index"
11044                    }
11045                    // PG-canonical bare `DROP <col>` without COLUMN
11046                    // keyword is also valid; treat any other ident
11047                    // as the column name.
11048                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11049                    other => {
11050                        return Err(self.err(alloc::format!(
11051                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11052                        )));
11053                    }
11054                };
11055                let mut if_exists = false;
11056                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11057                    let n1 = self.tokens.get(self.pos + 1);
11058                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11059                        self.advance();
11060                        self.advance();
11061                        if_exists = true;
11062                    }
11063                }
11064                let name = self.expect_ident_like()?;
11065                let mut cascade = false;
11066                if matches!(
11067                    self.peek(),
11068                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11069                        || s.eq_ignore_ascii_case("restrict")
11070                ) {
11071                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11072                    {
11073                        cascade = true;
11074                    }
11075                    self.advance();
11076                }
11077                if subject == "index" {
11078                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11079                        name,
11080                        if_exists,
11081                    }])
11082                } else if subject == "constraint" {
11083                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11084                        name,
11085                        if_exists,
11086                    }])
11087                } else {
11088                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11089                        column: name,
11090                        if_exists,
11091                        cascade,
11092                    }])
11093                }
11094            }
11095            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11096                self.advance();
11097                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11098                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11099                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11100                // immediately; accept-and-no-op.
11101                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11102                    self.advance();
11103                    self.consume_until_statement_boundary();
11104                    return Ok(Vec::new());
11105                }
11106                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11107                    self.advance();
11108                }
11109                let col_name = self.expect_ident_like()?;
11110                match self.peek() {
11111                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11112                        self.advance();
11113                    }
11114                    // v7.14.0 — pg_dump emits BIGSERIAL via
11115                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11116                    // nextval('seq')` (the sequence is created
11117                    // separately). SPG's BIGSERIAL already uses
11118                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11119                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11120                    // engine no-ops by consuming the tail.
11121                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11122                        // v7.22 (round-13 T2) — `SET DEFAULT
11123                        // nextval('…')` is how pg_dump spells a
11124                        // SERIAL column (plain integer in CREATE
11125                        // TABLE + this ALTER). It used to be
11126                        // swallowed as a no-op, which silently
11127                        // STRIPPED auto-increment from imported
11128                        // schemas — the first post-import INSERT
11129                        // without an explicit id then violated NOT
11130                        // NULL. Lower it to the auto-increment
11131                        // marker instead.
11132                        let is_default_nextval =
11133                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11134                                && matches!(
11135                                    self.tokens.get(self.pos + 2),
11136                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11137                                );
11138                        if is_default_nextval {
11139                            let seq_name = self.scan_sequence_name_until_boundary();
11140                            return Ok(alloc::vec![
11141                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11142                                    column: col_name,
11143                                    seq_name,
11144                                }
11145                            ]);
11146                        }
11147                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11148                        self.advance(); // consume "set"
11149                        match self.peek().clone() {
11150                            Token::Default => {
11151                                self.advance();
11152                                let default_expr = self.parse_expr(0)?;
11153                                return Ok(alloc::vec![
11154                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11155                                        column: col_name,
11156                                        default_expr,
11157                                    }
11158                                ]);
11159                            }
11160                            Token::Not => {
11161                                self.advance();
11162                                if !matches!(self.peek(), Token::Null) {
11163                                    return Err(self.err(alloc::format!(
11164                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11165                                        self.peek()
11166                                    )));
11167                                }
11168                                self.advance();
11169                                return Ok(alloc::vec![
11170                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11171                                        column: col_name,
11172                                    }
11173                                ]);
11174                            }
11175                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11176                            // stored generated column's expression and
11177                            // recompute existing rows.
11178                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11179                                self.advance(); // EXPRESSION
11180                                if matches!(self.peek(), Token::As) {
11181                                    self.advance();
11182                                }
11183                                let expr = self.parse_expr(0)?;
11184                                return Ok(alloc::vec![
11185                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11186                                        column: col_name,
11187                                        expr,
11188                                    }
11189                                ]);
11190                            }
11191                            other => {
11192                                // Other SET subjects (STATISTICS,
11193                                // STORAGE, COMPRESSION, …) stay no-ops —
11194                                // storage hints with no SPG semantics.
11195                                let _ = other;
11196                                self.consume_until_statement_boundary();
11197                                return Ok(Vec::new());
11198                            }
11199                        }
11200                    }
11201                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11202                        self.advance(); // consume "drop"
11203                        return self.parse_alter_column_drop_tail(col_name);
11204                    }
11205                    Token::Drop => {
11206                        self.advance(); // consume Drop token
11207                        return self.parse_alter_column_drop_tail(col_name);
11208                    }
11209                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11210                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11211                        // GENERATED { ALWAYS | BY DEFAULT } AS
11212                        // IDENTITY ( … )`: pg_dump's spelling for
11213                        // identity columns. Same auto-increment
11214                        // lowering as the nextval default; the
11215                        // sequence options inside the parens are
11216                        // no-ops under SPG's max+1 semantics.
11217                        let is_generated = matches!(
11218                            self.tokens.get(self.pos + 1),
11219                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11220                        );
11221                        if !is_generated {
11222                            return Err(self.err(alloc::format!(
11223                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11224                                self.tokens.get(self.pos + 1)
11225                            )));
11226                        }
11227                        let seq_name = self.scan_sequence_name_until_boundary();
11228                        return Ok(alloc::vec![
11229                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11230                                column: col_name,
11231                                seq_name,
11232                            }
11233                        ]);
11234                    }
11235                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11236                    // column: floor the next allocated value at n (bare
11237                    // RESTART = restart from the start value, 1).
11238                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11239                        self.advance();
11240                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11241                        {
11242                            self.advance();
11243                            let neg = if matches!(self.peek(), Token::Minus) {
11244                                self.advance();
11245                                true
11246                            } else {
11247                                false
11248                            };
11249                            match self.advance() {
11250                                Token::Integer(v) => Some(if neg { -v } else { v }),
11251                                other => {
11252                                    return Err(self.err(alloc::format!(
11253                                        "expected integer after RESTART WITH, got {other:?}"
11254                                    )));
11255                                }
11256                            }
11257                        } else {
11258                            None
11259                        };
11260                        return Ok(alloc::vec![
11261                            crate::ast::AlterTableTarget::AlterColumnRestart {
11262                                column: col_name,
11263                                with,
11264                            }
11265                        ]);
11266                    }
11267                    other => {
11268                        return Err(self.err(alloc::format!(
11269                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11270                        )));
11271                    }
11272                }
11273                // v7.39 (round 713) — the type parser has consumed a
11274                // trailing `COLLATE <name>` since Phase 2.5, and
11275                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11276                // TYPE text COLLATE "C"` parsed clean and changed
11277                // nothing. Keep the clause; the engine re-collates.
11278                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11279                    self.parse_type_with_implied_flags()?;
11280                let collation = if coll_explicit {
11281                    coll_name.map(|n| (coll, n))
11282                } else {
11283                    None
11284                };
11285                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11286                {
11287                    self.advance();
11288                    Some(self.parse_expr(0)?)
11289                } else {
11290                    None
11291                };
11292                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11293                    column: col_name,
11294                    new_type,
11295                    using,
11296                    collation,
11297                }])
11298            }
11299            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11300            // PG also supports `RENAME TO new_table` for table-name
11301            // rename; that surface is deferred (pg_dump never emits
11302            // it). If the first post-RENAME ident is `TO`, the user
11303            // is asking for table rename — error with a clear
11304            // message rather than misparsing `TO` as a column name.
11305            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11306                self.advance();
11307                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11308                // table-name rename (mailrs round-10 A.5 — used
11309                // by migrate-042's `RENAME TO email_contacts`).
11310                // `TO` lexes as Token::To.
11311                if matches!(self.peek(), Token::To)
11312                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11313                {
11314                    self.advance();
11315                    let new = self.expect_ident_like()?;
11316                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11317                        new,
11318                    }]);
11319                }
11320                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11321                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11322                    self.advance();
11323                    let old = self.expect_ident_like()?;
11324                    if matches!(self.peek(), Token::To) {
11325                        self.advance();
11326                    } else {
11327                        self.expect_keyword_ident("to")?;
11328                    }
11329                    let new = self.expect_ident_like()?;
11330                    return Ok(alloc::vec![
11331                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11332                    ]);
11333                }
11334                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11335                    self.advance();
11336                }
11337                let old = self.expect_ident_like()?;
11338                // `TO` is a reserved keyword token; accept both
11339                // Token::To and Token::Ident("to") for consistency.
11340                if matches!(self.peek(), Token::To) {
11341                    self.advance();
11342                } else {
11343                    self.expect_keyword_ident("to")?;
11344                }
11345                let new = self.expect_ident_like()?;
11346                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11347                    old,
11348                    new,
11349                }])
11350            }
11351            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11352            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11353            // every data block with these. Real disable semantics —
11354            // not no-op — because reload correctness assumes the
11355            // triggers don't fire (rows already carry their
11356            // computed values from prod).
11357            Token::Ident(s)
11358                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11359            {
11360                let enabled = s.eq_ignore_ascii_case("enable");
11361                self.advance();
11362                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11363                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11364                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11365                // pg_dump output) — anything else falls through to
11366                // the catch-all error below.
11367                // v7.22 (round-13 T3) — mysqldump wraps every data
11368                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11369                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11370                // maintains indexes incrementally — engine no-op.
11371                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11372                    self.advance();
11373                    return Ok(Vec::new());
11374                }
11375                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11376                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11377                // to gate triggers on session_replication_role; SPG
11378                // has no replica role, so the prefix is consumed and
11379                // treated identically to the plain ENABLE/DISABLE
11380                // TRIGGER form.
11381                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11382                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11383                {
11384                    self.advance();
11385                }
11386                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11387                    return Err(self.err(alloc::format!(
11388                        "expected TRIGGER after {}, got {:?}",
11389                        if enabled { "ENABLE" } else { "DISABLE" },
11390                        self.peek()
11391                    )));
11392                }
11393                self.advance();
11394                // `ALL` lexes as Token::All (reserved); also
11395                // accept Token::Ident("all") for symmetry.
11396                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11397                // TRIGGER selectors. USER (= all user triggers) is
11398                // semantically ALL here; REPLICA / ALWAYS gate on
11399                // session_replication_role which SPG doesn't track.
11400                // All map to TriggerSelector::All.
11401                let which = if matches!(self.peek(), Token::All)
11402                    || matches!(self.peek(), Token::Ident(s)
11403                        if s.eq_ignore_ascii_case("all")
11404                            || s.eq_ignore_ascii_case("user")
11405                            || s.eq_ignore_ascii_case("replica")
11406                            || s.eq_ignore_ascii_case("always"))
11407                {
11408                    self.advance();
11409                    crate::ast::TriggerSelector::All
11410                } else {
11411                    let name = self.expect_ident_like()?;
11412                    crate::ast::TriggerSelector::Named(name)
11413                };
11414                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11415                    which,
11416                    enabled,
11417                }])
11418            }
11419            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11420            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11421                self.advance();
11422                if !matches!(self.peek(), Token::Partition)
11423                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11424                        if s.eq_ignore_ascii_case("partition"))
11425                {
11426                    return Err(self.err(alloc::format!(
11427                        "expected PARTITION after ATTACH, got {:?}",
11428                        self.peek()
11429                    )));
11430                }
11431                self.advance();
11432                let child = self.expect_ident_like()?;
11433                let bounds = self.parse_partition_bounds_tail()?;
11434                Ok(alloc::vec![
11435                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11436                ])
11437            }
11438            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11439            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11440                self.advance();
11441                if !matches!(self.peek(), Token::Partition)
11442                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11443                        if s.eq_ignore_ascii_case("partition"))
11444                {
11445                    return Err(self.err(alloc::format!(
11446                        "expected PARTITION after DETACH, got {:?}",
11447                        self.peek()
11448                    )));
11449                }
11450                self.advance();
11451                let child = self.expect_ident_like()?;
11452                let mut concurrently = false;
11453                let mut finalize = false;
11454                loop {
11455                    match self.peek().clone() {
11456                        Token::Ident(s) | Token::QuotedIdent(s)
11457                            if s.eq_ignore_ascii_case("concurrently") =>
11458                        {
11459                            self.advance();
11460                            concurrently = true;
11461                        }
11462                        Token::Ident(s) | Token::QuotedIdent(s)
11463                            if s.eq_ignore_ascii_case("finalize") =>
11464                        {
11465                            self.advance();
11466                            finalize = true;
11467                        }
11468                        _ => break,
11469                    }
11470                }
11471                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11472                    child,
11473                    concurrently,
11474                    finalize,
11475                }])
11476            }
11477            other => Err(self.err(alloc::format!(
11478                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11479            ))),
11480        }
11481    }
11482
11483    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11484    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11485    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11486    /// `parse_partition_of_tail`'s bounds branch.
11487    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11488    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11489    /// lowering each to the respective AlterTableTarget. Any
11490    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11491    /// no-op via consume_until_statement_boundary.
11492    fn parse_alter_column_drop_tail(
11493        &mut self,
11494        col_name: String,
11495    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11496        match self.peek().clone() {
11497            Token::Default => {
11498                self.advance();
11499                Ok(alloc::vec![
11500                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11501                ])
11502            }
11503            Token::Not => {
11504                self.advance();
11505                if !matches!(self.peek(), Token::Null) {
11506                    return Err(self.err(alloc::format!(
11507                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11508                        self.peek()
11509                    )));
11510                }
11511                self.advance();
11512                Ok(alloc::vec![
11513                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11514                ])
11515            }
11516            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11517            // generated column into a plain column.
11518            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11519                self.advance();
11520                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11521                // dropped, so the engine still errored on a plain
11522                // column; PG's semantics are NOTICE + skip.
11523                let mut if_exists = false;
11524                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11525                    self.advance();
11526                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11527                        self.advance();
11528                        if_exists = true;
11529                    }
11530                }
11531                Ok(alloc::vec![
11532                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11533                        column: col_name,
11534                        if_exists,
11535                    }
11536                ])
11537            }
11538            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11539            // identity column into a plain column.
11540            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11541                self.advance();
11542                let mut if_exists = false;
11543                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11544                    self.advance();
11545                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11546                        self.advance();
11547                        if_exists = true;
11548                    }
11549                }
11550                Ok(alloc::vec![
11551                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11552                        column: col_name,
11553                        if_exists,
11554                    }
11555                ])
11556            }
11557            _ => {
11558                self.consume_until_statement_boundary();
11559                Ok(Vec::new())
11560            }
11561        }
11562    }
11563
11564    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11565    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11566    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11567    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11568    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11569        let mut opts = crate::ast::CopyOptions::default();
11570        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11571            return Ok(opts);
11572        }
11573        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11574            self.advance();
11575        }
11576        if matches!(self.peek(), Token::LParen) {
11577            self.advance();
11578            loop {
11579                self.parse_one_copy_option(&mut opts)?;
11580                match self.peek() {
11581                    Token::Comma => {
11582                        self.advance();
11583                    }
11584                    Token::RParen => {
11585                        self.advance();
11586                        break;
11587                    }
11588                    other => {
11589                        return Err(self.err(alloc::format!(
11590                            "expected ',' or ')' in COPY options, got {other:?}"
11591                        )));
11592                    }
11593                }
11594            }
11595        } else {
11596            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11597                self.parse_one_copy_option(&mut opts)?;
11598            }
11599        }
11600        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11601            return Err(self.err(alloc::format!(
11602                "unexpected token after COPY options: {:?}",
11603                self.peek()
11604            )));
11605        }
11606        Ok(opts)
11607    }
11608
11609    fn parse_one_copy_option(
11610        &mut self,
11611        opts: &mut crate::ast::CopyOptions,
11612    ) -> Result<(), ParseError> {
11613        use crate::ast::CopyFormat;
11614        // The option keyword. NULL lexes as its own token; the rest are
11615        // bare identifiers.
11616        let kw = match self.advance() {
11617            Token::Null => alloc::string::String::from("NULL"),
11618            Token::Ident(s) => s.to_uppercase(),
11619            other => {
11620                return Err(self.err(alloc::format!(
11621                    "expected a COPY option keyword, got {other:?}"
11622                )));
11623            }
11624        };
11625        match kw.as_str() {
11626            "FORMAT" => {
11627                let fmt = self.expect_ident_like()?;
11628                match fmt.to_ascii_uppercase().as_str() {
11629                    "CSV" => opts.format = CopyFormat::Csv,
11630                    "TEXT" => opts.format = CopyFormat::Text,
11631                    other => {
11632                        return Err(self.err(alloc::format!(
11633                            "COPY format \"{}\" not recognized",
11634                            other.to_ascii_lowercase()
11635                        )));
11636                    }
11637                }
11638            }
11639            // Legacy bare format keywords.
11640            "CSV" => opts.format = CopyFormat::Csv,
11641            "TEXT" => opts.format = CopyFormat::Text,
11642            "HEADER" => {
11643                opts.header = match self.peek() {
11644                    Token::True => {
11645                        self.advance();
11646                        true
11647                    }
11648                    Token::False => {
11649                        self.advance();
11650                        false
11651                    }
11652                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11653                        self.advance();
11654                        true
11655                    }
11656                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11657                        self.advance();
11658                        false
11659                    }
11660                    // Bare HEADER (no boolean) means HEADER true.
11661                    _ => true,
11662                };
11663            }
11664            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11665                let s = match self.advance() {
11666                    Token::String(s) => s,
11667                    other => {
11668                        return Err(self.err(alloc::format!(
11669                            "COPY {kw} expects a single-character string, got {other:?}"
11670                        )));
11671                    }
11672                };
11673                // v7.39 (round 247) — PG's wording (0A000), keyword in
11674                // lowercase: "COPY delimiter must be a single one-byte
11675                // character".
11676                let one_byte_err = || {
11677                    self.err(alloc::format!(
11678                        "COPY {} must be a single one-byte character",
11679                        kw.to_ascii_lowercase()
11680                    ))
11681                };
11682                let mut chars = s.chars();
11683                let c = chars.next().ok_or_else(one_byte_err)?;
11684                if chars.next().is_some() || c.len_utf8() != 1 {
11685                    return Err(one_byte_err());
11686                }
11687                match kw.as_str() {
11688                    "DELIMITER" => opts.delimiter = Some(c),
11689                    "QUOTE" => opts.quote = Some(c),
11690                    _ => opts.escape = Some(c),
11691                }
11692            }
11693            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11694            "FORCE_QUOTE" => {
11695                if matches!(self.peek(), Token::Star) {
11696                    self.advance();
11697                    opts.force_quote = Some(Vec::new());
11698                } else {
11699                    if !matches!(self.peek(), Token::LParen) {
11700                        return Err(self.err(alloc::format!(
11701                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11702                            self.peek()
11703                        )));
11704                    }
11705                    self.advance();
11706                    let mut cols = Vec::new();
11707                    loop {
11708                        cols.push(self.expect_ident_like()?);
11709                        match self.peek() {
11710                            Token::Comma => {
11711                                self.advance();
11712                            }
11713                            Token::RParen => {
11714                                self.advance();
11715                                break;
11716                            }
11717                            other => {
11718                                return Err(self.err(alloc::format!(
11719                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11720                                )));
11721                            }
11722                        }
11723                    }
11724                    opts.force_quote = Some(cols);
11725                }
11726            }
11727            "NULL" => {
11728                opts.null_str = Some(match self.advance() {
11729                    Token::String(s) => s,
11730                    other => {
11731                        return Err(self.err(alloc::format!(
11732                            "COPY NULL expects a quoted string, got {other:?}"
11733                        )));
11734                    }
11735                });
11736            }
11737            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11738            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11739            // FORCE_NULL too.
11740            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11741                let cols = self.parse_copy_column_list(&kw)?;
11742                if kw == "FORCE_NOT_NULL" {
11743                    opts.force_not_null = Some(cols);
11744                } else {
11745                    opts.force_null = Some(cols);
11746                }
11747            }
11748            other => {
11749                // PG's wording, lowercased option name.
11750                return Err(self.err(alloc::format!(
11751                    "option \"{}\" not recognized",
11752                    other.to_ascii_lowercase()
11753                )));
11754            }
11755        }
11756        Ok(())
11757    }
11758
11759    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11760    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11761    /// is the `*` spelling.
11762    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11763        if matches!(self.peek(), Token::Star) {
11764            self.advance();
11765            return Ok(Vec::new());
11766        }
11767        if !matches!(self.peek(), Token::LParen) {
11768            return Err(self.err(alloc::format!(
11769                "expected '(' or '*' after {kw}, got {:?}",
11770                self.peek()
11771            )));
11772        }
11773        self.advance();
11774        let mut cols = Vec::new();
11775        loop {
11776            cols.push(self.expect_ident_like()?);
11777            match self.peek() {
11778                Token::Comma => {
11779                    self.advance();
11780                }
11781                Token::RParen => {
11782                    self.advance();
11783                    break;
11784                }
11785                other => {
11786                    return Err(self.err(alloc::format!(
11787                        "expected ',' or ')' in {kw} list, got {other:?}"
11788                    )));
11789                }
11790            }
11791        }
11792        Ok(cols)
11793    }
11794
11795    fn parse_partition_bounds_tail(
11796        &mut self,
11797    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11798        use crate::ast::PartitionOfBoundsAst;
11799        match self.peek() {
11800            Token::Default => {
11801                self.advance();
11802                Ok(PartitionOfBoundsAst::Default)
11803            }
11804            Token::For => {
11805                self.advance();
11806                if !matches!(self.peek(), Token::Values) {
11807                    return Err(
11808                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11809                    );
11810                }
11811                self.advance();
11812                let want_with = matches!(
11813                    self.peek(),
11814                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11815                );
11816                if want_with {
11817                    self.advance();
11818                    if !matches!(self.peek(), Token::LParen) {
11819                        return Err(self.err(format!(
11820                            "expected '(' after FOR VALUES WITH, got {:?}",
11821                            self.peek()
11822                        )));
11823                    }
11824                    self.advance();
11825                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11826                    loop {
11827                        let key = self.expect_ident_like()?;
11828                        let n = match self.peek().clone() {
11829                            Token::Integer(v) if u32::try_from(v).is_ok() => {
11830                                self.advance();
11831                                v as u32
11832                            }
11833                            other => {
11834                                return Err(self.err(format!(
11835                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11836                                )));
11837                            }
11838                        };
11839                        match key.to_ascii_uppercase().as_str() {
11840                            "MODULUS" => modulus = Some(n),
11841                            "REMAINDER" => remainder = Some(n),
11842                            other => {
11843                                return Err(self.err(format!(
11844                                    "FOR VALUES WITH: unknown key {other:?}; \
11845                                     expected MODULUS or REMAINDER"
11846                                )));
11847                            }
11848                        }
11849                        match self.peek() {
11850                            Token::Comma => {
11851                                self.advance();
11852                            }
11853                            Token::RParen => {
11854                                self.advance();
11855                                break;
11856                            }
11857                            other => {
11858                                return Err(self.err(format!(
11859                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11860                                )));
11861                            }
11862                        }
11863                    }
11864                    let modulus = modulus
11865                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11866                    let remainder = remainder.ok_or_else(|| {
11867                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11868                    })?;
11869                    if modulus == 0 {
11870                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11871                    }
11872                    if remainder >= modulus {
11873                        return Err(self.err(format!(
11874                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11875                        )));
11876                    }
11877                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11878                }
11879                match self.peek() {
11880                    Token::From => {
11881                        self.advance();
11882                        let lower = Box::new(self.parse_partition_bound_expr()?);
11883                        if !matches!(self.peek(), Token::To) {
11884                            return Err(self.err(format!(
11885                                "expected TO after FROM (...), got {:?}",
11886                                self.peek()
11887                            )));
11888                        }
11889                        self.advance();
11890                        let upper = Box::new(self.parse_partition_bound_expr()?);
11891                        Ok(PartitionOfBoundsAst::Range { lower, upper })
11892                    }
11893                    Token::In => {
11894                        self.advance();
11895                        if !matches!(self.peek(), Token::LParen) {
11896                            return Err(self.err(format!(
11897                                "expected '(' after FOR VALUES IN, got {:?}",
11898                                self.peek()
11899                            )));
11900                        }
11901                        self.advance();
11902                        let mut values = Vec::new();
11903                        loop {
11904                            values.push(self.parse_expr(0)?);
11905                            match self.peek() {
11906                                Token::Comma => {
11907                                    self.advance();
11908                                }
11909                                Token::RParen => {
11910                                    self.advance();
11911                                    break;
11912                                }
11913                                other => {
11914                                    return Err(self.err(format!(
11915                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11916                                    )));
11917                                }
11918                            }
11919                        }
11920                        if values.is_empty() {
11921                            return Err(
11922                                self.err("FOR VALUES IN requires at least one literal".to_string())
11923                            );
11924                        }
11925                        Ok(PartitionOfBoundsAst::List { values })
11926                    }
11927                    other => Err(self.err(format!(
11928                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11929                    ))),
11930                }
11931            }
11932            other => Err(self.err(format!(
11933                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11934            ))),
11935        }
11936    }
11937
11938    /// v7.16.2 — peek for `information_schema.<tbl>` /
11939    /// `pg_catalog.<tbl>` triples and, if matched, consume all
11940    /// three tokens + return a synthetic table name the engine's
11941    /// SELECT path recognises as a virtual view. Returns `None`
11942    /// when the head doesn't look like a meta-qualified name.
11943    /// Used by `parse_table_ref` to bypass the
11944    /// `expect_ident_like` schema-strip for these specific PG
11945    /// meta schemas (mailrs round-10 A.3).
11946    fn try_peek_meta_qualified(&mut self) -> Option<String> {
11947        // Extract the schema name. Must be a plain ident token.
11948        let schema = match self.tokens.get(self.pos) {
11949            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11950            _ => return None,
11951        };
11952        // Dot.
11953        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11954            return None;
11955        }
11956        // The table-side ident may lex as a reserved keyword
11957        // (e.g. `Token::Tables`). Tolerate the common ones via a
11958        // helper that reads the trailing token's underlying name.
11959        let tbl = match self.tokens.get(self.pos + 2)? {
11960            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11961            Token::Tables => "tables".to_string(),
11962            // Other PG meta table names that may collide with
11963            // reserved keywords land here as needed.
11964            _ => return None,
11965        };
11966        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11967        // names so the synthetic name doesn't double-prefix
11968        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11969        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11970            ("__spg_info_", tbl.to_ascii_lowercase())
11971        } else if schema.eq_ignore_ascii_case("pg_catalog") {
11972            // v7.39 (round 541) — only the catalogs SPG actually
11973            // synthesises are rewritten, which is what the BARE path
11974            // has always checked. Anything else keeps its own name and
11975            // takes the ordinary route: `pg_stat_activity` and friends
11976            // resolve through meta_view_result, and a name that is no
11977            // catalog at all gets PG's "relation does not exist"
11978            // instead of a message about a view SPG cannot materialise.
11979            let lowered = tbl.to_ascii_lowercase();
11980            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11981                self.advance(); // schema
11982                self.advance(); // dot
11983                self.advance(); // tbl
11984                return Some(lowered);
11985            }
11986            let bare = lowered
11987                .strip_prefix("pg_")
11988                .map(alloc::string::String::from)
11989                .unwrap_or(lowered);
11990            ("__spg_pg_", bare)
11991        } else if schema.eq_ignore_ascii_case("mysql") {
11992            // v7.17.0 Phase 3.P0-65 — MySQL system schema
11993            // (`mysql.user`, `mysql.db`). Same synthetic-name
11994            // shape as pg_catalog.
11995            ("__spg_mysql_", tbl.to_ascii_lowercase())
11996        } else {
11997            return None;
11998        };
11999        self.advance(); // schema
12000        self.advance(); // dot
12001        self.advance(); // tbl
12002        Some(alloc::format!("{prefix}{normalised}"))
12003    }
12004
12005    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12006    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12007    /// implicit front of every search_path, so a bare reference to a
12008    /// known catalog table always means the catalog table. Only the
12009    /// names the engine actually synthesises are recognised — any
12010    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12011    fn try_peek_meta_bare(&mut self) -> Option<String> {
12012        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12013        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12014        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12015        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12016        // through the meta_view_result path instead, and already resolve
12017        // bare — they must NOT be listed here or the __spg_ rewrite would
12018        // mis-target them.)
12019        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12020        let name = match self.tokens.get(self.pos) {
12021            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12022            _ => return None,
12023        };
12024        // A following dot means this ident is a schema qualifier,
12025        // not a table name — let the qualified path handle it.
12026        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12027            return None;
12028        }
12029        if !PG_META_TABLES.contains(&name.as_str()) {
12030            return None;
12031        }
12032        self.advance();
12033        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12034        Some(alloc::format!("__spg_pg_{bare}"))
12035    }
12036
12037    /// Consume a bare ident if its lowercase matches `kw`, else err.
12038    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12039    /// Peeks only; the caller advances.
12040    fn peek_keyword_ident(&self, kw: &str) -> bool {
12041        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12042    }
12043
12044    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12045        match self.advance() {
12046            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12047            other => Err(ParseError {
12048                message: format!("expected {kw:?}, got {other:?}"),
12049                token_pos: self.consumed_pos(),
12050            }),
12051        }
12052    }
12053
12054    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12055    /// literal (`'foo'`) — same shape used by CREATE USER for the
12056    /// username slot.
12057    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12058        match self.advance() {
12059            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12060            other => Err(ParseError {
12061                message: format!("expected identifier or string, got {other:?}"),
12062                token_pos: self.consumed_pos(),
12063            }),
12064        }
12065    }
12066
12067    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12068        match self.advance() {
12069            Token::String(s) => Ok(s),
12070            other => Err(ParseError {
12071                message: format!("expected quoted string, got {other:?}"),
12072                token_pos: self.consumed_pos(),
12073            }),
12074        }
12075    }
12076
12077    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12078        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12079        // subqueries recurse through here without passing
12080        // parse_expr; share the same nesting budget.
12081        self.enter_nested()?;
12082        let r = self.parse_select_stmt_inner();
12083        self.nest_depth -= 1;
12084        r
12085    }
12086
12087    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12088        // Caller dispatches on Token::Select; the inner helper handles
12089        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12090        // get a fresh bare-select parse and may not have their own ORDER
12091        // BY / LIMIT.
12092        let mut head = self.parse_bare_select()?;
12093        self.parse_setop_chain_into(&mut head)?;
12094        self.parse_select_tail_into(&mut head)?;
12095        Ok(Statement::Select(head))
12096    }
12097
12098    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12099    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12100    /// token), and INTERSECT [ALL] (a bare ident — it was never
12101    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12102    /// tighter than UNION / EXCEPT — the executor folds the chain
12103    /// left-to-right, which is already correct for LEADING
12104    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12105    /// pair nests into that previous peer, so A UNION B INTERSECT C
12106    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12107    /// groups.
12108    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12109        // A parenthesized group arrives with its own (already
12110        // regrouped) unions on `head`; only the pairs THIS chain
12111        // appends participate in the precedence regroup below —
12112        // nesting an outer INTERSECT into a group-internal peer
12113        // would dissolve the explicit grouping.
12114        let boundary = head.unions.len();
12115        loop {
12116            let base = match self.peek() {
12117                Token::Union => UnionKind::Distinct,
12118                Token::Except => UnionKind::Except,
12119                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12120                _ => break,
12121            };
12122            self.advance();
12123            let kind = if matches!(self.peek(), Token::All) {
12124                self.advance();
12125                match base {
12126                    UnionKind::Distinct => UnionKind::All,
12127                    UnionKind::Except => UnionKind::ExceptAll,
12128                    _ => UnionKind::IntersectAll,
12129                }
12130            } else {
12131                base
12132            };
12133            let peer = self.parse_bare_select()?;
12134            head.unions.push((kind, peer));
12135        }
12136        let mut pairs = core::mem::take(&mut head.unions);
12137        let tail = pairs.split_off(boundary);
12138        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12139        for (kind, peer) in tail {
12140            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12141            // An intersect nests into the previous element of THIS
12142            // chain only; with no new previous element it stays at
12143            // the outer level (the left fold applies it to the
12144            // whole head, group included).
12145            match (
12146                is_intersect,
12147                regrouped.len() > boundary,
12148                regrouped.last_mut(),
12149            ) {
12150                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12151                _ => regrouped.push((kind, peer)),
12152            }
12153        }
12154        head.unions = regrouped;
12155        Ok(())
12156    }
12157
12158    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12159    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12160    /// the top-level bare VALUES statement reuses it verbatim.
12161    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12162    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12163    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12164    /// where the grouping-set universe is still in scope.
12165    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12166        if !matches!(self.peek(), Token::Order) {
12167            return Ok(Vec::new());
12168        }
12169        self.advance();
12170        if !self.peek_is_by() {
12171            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12172        }
12173        self.advance();
12174        let mut keys = Vec::new();
12175        loop {
12176            // v7.39 (round 691) — save/restore, the discipline this parser
12177            // already uses around `pending_sample_preds`, so a subquery inside
12178            // a key neither inherits nor leaks the channel.
12179            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12180            let saved_coll = self.order_key_collation.take();
12181            let parsed = self.parse_expr(0);
12182            self.in_order_by_key = saved_flag;
12183            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12184            let expr = parsed?;
12185            let desc = if matches!(self.peek(), Token::Desc) {
12186                self.advance();
12187                true
12188            } else if matches!(self.peek(), Token::Asc) {
12189                self.advance();
12190                false
12191            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12192                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12193                // one ordering per type, so the btree comparison operators map
12194                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12195                // would need a custom operator class — honest error.
12196                self.advance();
12197                match self.advance() {
12198                    Token::Lt | Token::LtEq => false,
12199                    Token::Gt | Token::GtEq => true,
12200                    other => {
12201                        return Err(self.err(alloc::format!(
12202                            "ORDER BY USING supports the btree comparison \
12203                             operators (< <= > >=); got {other:?}"
12204                        )));
12205                    }
12206                }
12207            } else {
12208                false
12209            };
12210            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12211            let nulls_first = self.parse_optional_nulls_placement()?;
12212            keys.push(OrderBy {
12213                expr,
12214                desc,
12215                nulls_first,
12216                collation,
12217            });
12218            if matches!(self.peek(), Token::Comma) {
12219                self.advance();
12220            } else {
12221                break;
12222            }
12223        }
12224        Ok(keys)
12225    }
12226
12227    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12228        // v7.39 (round 135) — a grouping-set query may have already parsed +
12229        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12230        // no ORDER BY token is present, keep that pre-set order_by rather than
12231        // clobbering it with an empty list.
12232        let parsed_keys = self.parse_order_by_keys()?;
12233        head.order_by = if parsed_keys.is_empty() {
12234            core::mem::take(&mut head.order_by)
12235        } else {
12236            parsed_keys
12237        };
12238        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12239        // order. PG's grammar takes a limit clause and an offset clause
12240        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12241        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12242        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12243        // spelling died on `expected end of input, got Limit`.
12244        //
12245        // Each may appear at most once, and LIMIT and FETCH FIRST are
12246        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12247        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12248        // A second one is left unconsumed here, which the caller reports
12249        // as trailing input rather than silently taking the last.
12250        let mut saw_limit = false;
12251        let mut saw_offset = false;
12252        loop {
12253            if !saw_limit && matches!(self.peek(), Token::Limit) {
12254                self.advance();
12255                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12256                // PG synonyms for "no limit". Treat both as None
12257                // (no head.limit set) so the engine's existing
12258                // unlimited-result path takes over. Reject was the
12259                // pre-5.1 behaviour and broke pg_dump-flavoured
12260                // tooling that occasionally emits LIMIT NULL.
12261                if self.consume_limit_unbounded_sentinel() {
12262                    head.limit = None;
12263                } else {
12264                    let first = self.parse_limit_expr("LIMIT")?;
12265                    // MySQL `LIMIT offset, count` — the first number is
12266                    // the offset when a comma follows.
12267                    if matches!(self.peek(), Token::Comma) {
12268                        self.advance();
12269                        let count = self.parse_limit_expr("LIMIT")?;
12270                        head.offset = Some(first);
12271                        saw_offset = true;
12272                        head.limit = Some(count);
12273                    } else {
12274                        head.limit = Some(first);
12275                    }
12276                }
12277                saw_limit = true;
12278                continue;
12279            }
12280            if !saw_offset && matches!(self.peek(), Token::Offset) {
12281                self.advance();
12282                // PG also accepts an optional `ROW` / `ROWS` trailer
12283                // after the offset value (`OFFSET 10 ROWS`). The
12284                // FETCH-FIRST branch below relies on the same.
12285                let off = self.parse_limit_expr("OFFSET")?;
12286                self.consume_optional_rows_keyword();
12287                head.offset = Some(off);
12288                saw_offset = true;
12289                continue;
12290            }
12291            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12292            // the SQL-standard alias for LIMIT. PG accepts both
12293            // spellings interchangeably; pg_dump emits FETCH FIRST in
12294            // newer versions. We map it onto `head.limit` so the
12295            // engine path is unified.
12296            if !saw_limit
12297                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12298                    if s.eq_ignore_ascii_case("fetch"))
12299            {
12300                self.advance(); // FETCH
12301                // `FIRST` or `NEXT` (both legal per SQL standard).
12302                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12303                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12304                {
12305                    self.advance();
12306                }
12307                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12308                // implicit 1 — but we always consume one if present).
12309                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12310                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12311                {
12312                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12313                    crate::ast::LimitExpr::Literal(1)
12314                } else {
12315                    self.parse_limit_expr("FETCH FIRST")?
12316                };
12317                // Eat `ROW` / `ROWS` if not already consumed above.
12318                self.consume_optional_rows_keyword();
12319                // Optional `ONLY` (the spec form) — or the SQL:2008
12320                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12321                // now honours WITH TIES by extending past the LIMIT
12322                // truncation point through every row that shares the
12323                // last-kept row's ORDER BY key.
12324                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12325                    if s.eq_ignore_ascii_case("only"))
12326                {
12327                    self.advance();
12328                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12329                    if s.eq_ignore_ascii_case("with"))
12330                {
12331                    self.advance(); // WITH
12332                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12333                        if s.eq_ignore_ascii_case("ties"))
12334                    {
12335                        self.advance();
12336                        head.limit_with_ties = true;
12337                    }
12338                }
12339                head.limit = Some(count);
12340                saw_limit = true;
12341                continue;
12342            }
12343            break;
12344        }
12345        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12346        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12347        //       [ OF table_name [, …] ]
12348        //       [ NOWAIT | SKIP LOCKED ]
12349        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12350        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12351        // SELECT already returns a consistent snapshot — so these
12352        // are accept-and-discard: the parser absorbs them so
12353        // mailrs / Rails / Django code paths that emit `SELECT
12354        // … FOR UPDATE` for advisory pessimistic locking load
12355        // without a parser error. The on-disk locking model is
12356        // unchanged; callers that rely on FOR UPDATE for read-
12357        // through-write ordering still get the right answer
12358        // because SPG serialises writes anyway.
12359        head.locking = self
12360            .consume_optional_for_lock_clauses()
12361            .map(alloc::boxed::Box::new);
12362        Ok(())
12363    }
12364
12365    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12366    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12367    /// LOCKED ]` trailers. Each clause is fully accepted and
12368    /// discarded — SPG's single-writer model already satisfies the
12369    /// callers' implicit ordering requirement. Stops at the first
12370    /// token that isn't `FOR`.
12371    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12372        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12373        // not discarded. PG keeps the strongest of several clauses; the
12374        // policy of the last one wins, which is what this loop records.
12375        let mut seen: Option<crate::ast::LockingClause> = None;
12376        while matches!(self.peek(), Token::For) {
12377            // v7.37.14 (A2.5-stub) — record that this query asked
12378            // for a row lock the parser is about to silently
12379            // discard. Operators surface the count via
12380            // `spg_sql::silent_for_update_count()` so they can
12381            // gauge how much of the workload depends on advisory
12382            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12383            // before v7.37.15's per-row tuple locking lands.
12384            crate::record_silent_for_update_clause();
12385            self.advance(); // FOR
12386            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12387            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12388            let mut no_key = false;
12389            let mut key = false;
12390            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12391                if s.eq_ignore_ascii_case("no"))
12392            {
12393                self.advance(); // NO
12394                no_key = true;
12395                // The next ident should be KEY but be generous;
12396                // anything followed by UPDATE/SHARE is accepted.
12397                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12398                    if s.eq_ignore_ascii_case("key"))
12399                {
12400                    self.advance(); // KEY
12401                }
12402            }
12403            // `KEY` prefix (PG `FOR KEY SHARE`).
12404            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12405                if s.eq_ignore_ascii_case("key"))
12406            {
12407                self.advance(); // KEY
12408                key = true;
12409            }
12410            // Lock-strength keyword: UPDATE / SHARE. Required, but
12411            // we're lenient — an unexpected token here just bails
12412            // (we already consumed FOR; caller's downstream
12413            // dispatch will error if anything actually depends on
12414            // the trailing tokens).
12415            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12416                if s.eq_ignore_ascii_case("update"));
12417            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12419            {
12420                self.advance();
12421                use crate::ast::LockStrength as LS;
12422                let strength = match (is_update, no_key, key) {
12423                    (true, true, _) => LS::NoKeyUpdate,
12424                    (true, _, _) => LS::Update,
12425                    (false, _, true) => LS::KeyShare,
12426                    (false, _, _) => LS::Share,
12427                };
12428                seen = Some(crate::ast::LockingClause {
12429                    strength,
12430                    of_tables: alloc::vec::Vec::new(),
12431                    policy: crate::ast::LockWait::Wait,
12432                });
12433            } else {
12434                // FOR by itself (or `FOR KEY` with nothing after) —
12435                // give up on the lock-clause path. We've already
12436                // advanced past FOR; further attempts to parse
12437                // here would clobber state.
12438                return seen;
12439            }
12440            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12441            // joining and locking only a subset of tables.
12442            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12443                if s.eq_ignore_ascii_case("of"))
12444            {
12445                self.advance(); // OF
12446                #[allow(clippy::while_let_loop)]
12447                loop {
12448                    match self.peek() {
12449                        Token::Ident(_) | Token::QuotedIdent(_) => {
12450                            // v7.39 (round 294) — the name is CAPTURED now: PG
12451                            // validates it against the FROM clause, and an
12452                            // uncaptured list silently means "lock everything".
12453                            let mut nm = match self.advance() {
12454                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12455                                _ => alloc::string::String::new(),
12456                            };
12457                            // Optional schema-qualified `schema.table`.
12458                            if matches!(self.peek(), Token::Dot) {
12459                                self.advance();
12460                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12461                                {
12462                                    self.advance();
12463                                    nm = n;
12464                                }
12465                            }
12466                            if let Some(c) = seen.as_mut() {
12467                                c.of_tables.push(nm);
12468                            }
12469                        }
12470                        _ => break,
12471                    }
12472                    if matches!(self.peek(), Token::Comma) {
12473                        self.advance();
12474                    } else {
12475                        break;
12476                    }
12477                }
12478            }
12479            // Optional `NOWAIT` | `SKIP LOCKED`.
12480            match self.peek().clone() {
12481                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12482                    self.advance();
12483                    if let Some(c) = seen.as_mut() {
12484                        c.policy = crate::ast::LockWait::NoWait;
12485                    }
12486                }
12487                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12488                    self.advance(); // SKIP
12489                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12490                        if s.eq_ignore_ascii_case("locked"))
12491                    {
12492                        self.advance(); // LOCKED
12493                        if let Some(c) = seen.as_mut() {
12494                            c.policy = crate::ast::LockWait::SkipLocked;
12495                        }
12496                    }
12497                }
12498                _ => {}
12499            }
12500            // Loop: PG allows multiple FOR clauses chained.
12501        }
12502        seen
12503    }
12504
12505    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12506    /// Bind value gets resolved during prepared-statement Execute;
12507    /// the Pratt expression parser would over-accept here (e.g.
12508    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12509    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12510    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12511    /// when one was consumed; caller skips the regular
12512    /// limit-value parse and leaves `head.limit` at None.
12513    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12514        if matches!(self.peek(), Token::Null) {
12515            self.advance();
12516            return true;
12517        }
12518        if matches!(self.peek(), Token::All) {
12519            self.advance();
12520            return true;
12521        }
12522        false
12523    }
12524
12525    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12526    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12527    /// SQL-standard shape. No-op when missing.
12528    fn consume_optional_rows_keyword(&mut self) {
12529        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12530            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12531        {
12532            self.advance();
12533        }
12534    }
12535
12536    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12537    ///
12538    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12539    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12540    /// constant, which is why that spelling keeps the token path below.
12541    ///
12542    /// Constants are folded here rather than carried into the tree: the
12543    /// 15+ execution paths that read the row count go through
12544    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12545    /// means "no limit". A clause the engine could not resolve would
12546    /// therefore return the WHOLE table instead of failing. Folding at
12547    /// parse time keeps that impossible; a non-constant clause is still
12548    /// a clean error (recorded residual — closing it wants a resolution
12549    /// pre-pass on the simple-query path, where `substitute_placeholders`
12550    /// does not run).
12551    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12552        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12553        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12554        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12555        // ONLY` both work (its grammar takes a c_expr). Both measured
12556        // against PG 18.4 in round 305.
12557        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12558            return self.parse_limit_constant(label);
12559        }
12560        // One pass, no rewind: `advance()` takes each token by
12561        // `mem::replace`, so a consumed token reads back as Eof and this
12562        // parser cannot backtrack. Everything — bare literal included —
12563        // is therefore folded from the parsed expression rather than
12564        // re-read from the token stream.
12565        let start = self.pos;
12566        let e = self.parse_expr(0)?;
12567        if let crate::ast::Expr::Placeholder(n) = e {
12568            return Ok(crate::ast::LimitExpr::Placeholder(n));
12569        }
12570        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12571        match fold_limit_constant(&e) {
12572            Some(Ok(v)) if v < 0 => Err(ParseError {
12573                message: alloc::format!("{neg_label} must not be negative"),
12574                token_pos: start,
12575            }),
12576            Some(Ok(v)) => u32::try_from(v)
12577                .map(crate::ast::LimitExpr::Literal)
12578                .map_err(|_| ParseError {
12579                    message: alloc::format!("{label} value too large: {v}"),
12580                    token_pos: start,
12581                }),
12582            Some(Err(message)) => Err(ParseError {
12583                message: message.replace("{L}", neg_label),
12584                token_pos: start,
12585            }),
12586            // v7.39 (round 305, V23) — not foldable at parse time
12587            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12588            // expression; the engine evaluates it once before dispatch.
12589            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12590        }
12591    }
12592
12593    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12594        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12595        // coercion rules, not just an integer token: a NUMERIC rounds half
12596        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12597        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12598        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12599        // content, failing as an input-syntax error on the value. General
12600        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12601        // they need an Expr-carrying LimitExpr variant.
12602        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12603        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12604            message,
12605            token_pos: pos,
12606        };
12607        match self.advance() {
12608            Token::Integer(n) if n >= 0 => u32::try_from(n)
12609                .map(crate::ast::LimitExpr::Literal)
12610                .map_err(|_| ParseError {
12611                    message: alloc::format!("{label} value too large: {n}"),
12612                    token_pos: self.consumed_pos(),
12613                }),
12614            Token::Integer(_) => Err(err_at(
12615                alloc::format!("{neg_label} must not be negative"),
12616                self.pos.saturating_sub(1),
12617            )),
12618            Token::Numeric(t) => {
12619                let pos = self.pos.saturating_sub(1);
12620                let v: f64 = t.parse().map_err(|_| {
12621                    err_at(
12622                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12623                        pos,
12624                    )
12625                })?;
12626                if v < 0.0 {
12627                    return Err(err_at(
12628                        alloc::format!("{neg_label} must not be negative"),
12629                        pos,
12630                    ));
12631                }
12632                // Round half away from zero — PG's numeric→bigint cast.
12633                // (no_std: no f64::round; v is non-negative, so truncating
12634                // v + 0.5 is the same thing.)
12635                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12636                let rounded = (v + 0.5) as u64;
12637                u32::try_from(rounded)
12638                    .map(crate::ast::LimitExpr::Literal)
12639                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12640            }
12641            Token::Minus => {
12642                let pos = self.pos.saturating_sub(1);
12643                match self.peek() {
12644                    Token::Integer(_) | Token::Numeric(_) => {
12645                        self.advance();
12646                        Err(err_at(
12647                            alloc::format!("{neg_label} must not be negative"),
12648                            pos,
12649                        ))
12650                    }
12651                    other => Err(err_at(
12652                        alloc::format!(
12653                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12654                        ),
12655                        pos,
12656                    )),
12657                }
12658            }
12659            Token::String(t) => {
12660                let pos = self.pos.saturating_sub(1);
12661                match t.trim().parse::<i64>() {
12662                    Ok(n) if n < 0 => Err(err_at(
12663                        alloc::format!("{neg_label} must not be negative"),
12664                        pos,
12665                    )),
12666                    Ok(n) => u32::try_from(n)
12667                        .map(crate::ast::LimitExpr::Literal)
12668                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12669                    Err(_) => Err(err_at(
12670                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12671                        pos,
12672                    )),
12673                }
12674            }
12675            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12676            other => Err(ParseError {
12677                message: alloc::format!(
12678                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12679                ),
12680                token_pos: self.consumed_pos(),
12681            }),
12682        }
12683    }
12684
12685    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12686    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12687    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12688    /// `parse_select_stmt` is responsible for filling those in.
12689    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12690    /// call in the expression tree to the per-set integer bitmask
12691    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12692    /// is dropped in this grouping set). Runs during the ROLLUP /
12693    /// CUBE / GROUPING SETS expansion, where the set is known.
12694    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12695    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12696    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12697        if let Expr::FunctionCall { name, .. } = expr
12698            && name.eq_ignore_ascii_case("grouping")
12699        {
12700            if !out.iter().any(|e| e == expr) {
12701                out.push(expr.clone());
12702            }
12703            return;
12704        }
12705        match expr {
12706            Expr::Binary { lhs, rhs, .. } => {
12707                Self::collect_grouping_calls(lhs, out);
12708                Self::collect_grouping_calls(rhs, out);
12709            }
12710            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12711                Self::collect_grouping_calls(expr, out)
12712            }
12713            Expr::FunctionCall { args, .. } => {
12714                for a in args {
12715                    Self::collect_grouping_calls(a, out);
12716                }
12717            }
12718            Expr::Case {
12719                operand,
12720                branches,
12721                else_branch,
12722            } => {
12723                if let Some(o) = operand {
12724                    Self::collect_grouping_calls(o, out);
12725                }
12726                for (c, v) in branches {
12727                    Self::collect_grouping_calls(c, out);
12728                    Self::collect_grouping_calls(v, out);
12729                }
12730                if let Some(x) = else_branch {
12731                    Self::collect_grouping_calls(x, out);
12732                }
12733            }
12734            _ => {}
12735        }
12736    }
12737
12738    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12739    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12740    /// `__grp_ord_k` (injected per grouping-set branch).
12741    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12742        if let Expr::FunctionCall { name, .. } = expr
12743            && name.eq_ignore_ascii_case("grouping")
12744        {
12745            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12746                *expr = Expr::Column(crate::ast::ColumnName {
12747                    qualifier: None,
12748                    name: alloc::format!("__grp_ord_{k}"),
12749                });
12750            }
12751            return;
12752        }
12753        match expr {
12754            Expr::Binary { lhs, rhs, .. } => {
12755                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12756                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12757            }
12758            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12759                Self::rewrite_grouping_to_col(expr, grp_exprs)
12760            }
12761            Expr::FunctionCall { args, .. } => {
12762                for a in args {
12763                    Self::rewrite_grouping_to_col(a, grp_exprs);
12764                }
12765            }
12766            Expr::Case {
12767                operand,
12768                branches,
12769                else_branch,
12770            } => {
12771                if let Some(o) = operand {
12772                    Self::rewrite_grouping_to_col(o, grp_exprs);
12773                }
12774                for (c, v) in branches {
12775                    Self::rewrite_grouping_to_col(c, grp_exprs);
12776                    Self::rewrite_grouping_to_col(v, grp_exprs);
12777                }
12778                if let Some(x) = else_branch {
12779                    Self::rewrite_grouping_to_col(x, grp_exprs);
12780                }
12781            }
12782            _ => {}
12783        }
12784    }
12785
12786    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12787    /// as the list of key sets it contributes. A bare expression is one
12788    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12789    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12790    /// the concatenation of its items' sets, where an item is itself an
12791    /// element, a parenthesized key list, or the empty set `()`. A
12792    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12793    /// move together.
12794    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12795        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12796        // ROLLUP ( … ) / CUBE ( … )
12797        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12798            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12799        {
12800            let is_cube = is_kw(self.peek(), "cube");
12801            self.advance(); // ROLLUP / CUBE
12802            self.advance(); // (
12803            let mut units: Vec<Vec<Expr>> = Vec::new();
12804            loop {
12805                if matches!(self.peek(), Token::LParen) {
12806                    // Composite unit: (a, b) rolls up as one.
12807                    self.advance();
12808                    let mut unit = Vec::new();
12809                    if !matches!(self.peek(), Token::RParen) {
12810                        loop {
12811                            unit.push(self.parse_expr(0)?);
12812                            match self.peek() {
12813                                Token::Comma => {
12814                                    self.advance();
12815                                }
12816                                Token::RParen => break,
12817                                other => {
12818                                    return Err(self.err(format!(
12819                                        "expected ',' or ')' in grouping unit, got {other:?}"
12820                                    )));
12821                                }
12822                            }
12823                        }
12824                    }
12825                    self.advance(); // )
12826                    units.push(unit);
12827                } else {
12828                    units.push(alloc::vec![self.parse_expr(0)?]);
12829                }
12830                match self.peek() {
12831                    Token::Comma => {
12832                        self.advance();
12833                    }
12834                    Token::RParen => break,
12835                    other => {
12836                        return Err(self.err(format!(
12837                            "expected ',' or ')' in grouping list, got {other:?}"
12838                        )));
12839                    }
12840                }
12841            }
12842            self.advance(); // )
12843            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12844                units
12845                    .iter()
12846                    .zip(unit_sel.iter())
12847                    .filter(|(_, keep)| **keep)
12848                    .flat_map(|(u, _)| u.iter().cloned())
12849                    .collect()
12850            };
12851            let n = units.len();
12852            if is_cube {
12853                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12854                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12855                    .collect();
12856                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12857                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12858            }
12859            return Ok((0..=n)
12860                .rev()
12861                .map(|keep| {
12862                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12863                    flatten(&sel)
12864                })
12865                .collect());
12866        }
12867        // GROUPING SETS ( item [, item]* )
12868        if is_kw(self.peek(), "grouping")
12869            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12870        {
12871            self.advance(); // GROUPING
12872            self.advance(); // SETS
12873            if !matches!(self.peek(), Token::LParen) {
12874                return Err(self.err(format!(
12875                    "expected '(' after GROUPING SETS, got {:?}",
12876                    self.peek()
12877                )));
12878            }
12879            self.advance(); // outer (
12880            let mut sets: Vec<Vec<Expr>> = Vec::new();
12881            loop {
12882                if matches!(self.peek(), Token::LParen) {
12883                    // A parenthesized key list (or the empty set).
12884                    self.advance();
12885                    let mut set = Vec::new();
12886                    if !matches!(self.peek(), Token::RParen) {
12887                        loop {
12888                            set.push(self.parse_expr(0)?);
12889                            match self.peek() {
12890                                Token::Comma => {
12891                                    self.advance();
12892                                }
12893                                Token::RParen => break,
12894                                other => {
12895                                    return Err(self.err(format!(
12896                                        "expected ',' or ')' in grouping set, got {other:?}"
12897                                    )));
12898                                }
12899                            }
12900                        }
12901                    }
12902                    self.advance(); // )
12903                    sets.push(set);
12904                } else {
12905                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12906                    // bare expression.
12907                    sets.extend(self.parse_grouping_element()?);
12908                }
12909                match self.peek() {
12910                    Token::Comma => {
12911                        self.advance();
12912                    }
12913                    Token::RParen => break,
12914                    other => {
12915                        return Err(self.err(format!(
12916                            "expected ',' or ')' after a grouping set, got {other:?}"
12917                        )));
12918                    }
12919                }
12920            }
12921            self.advance(); // outer )
12922            return Ok(sets);
12923        }
12924        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12925    }
12926
12927    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12928        // v7.38 (read01) — a reference to a key that is dropped in this grouping
12929        // set evaluates to NULL, at any depth. Previously only a *top-level*
12930        // select item equal to a dropped key was nullified, so a key nested in
12931        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12932        // column and failed to resolve against the set's synthetic schema.
12933        if dropped.iter().any(|d| d == expr) {
12934            *expr = Expr::Literal(Literal::Null);
12935            return;
12936        }
12937        if let Expr::FunctionCall { name, args } = expr
12938            && name.eq_ignore_ascii_case("grouping")
12939        {
12940            let mut mask: i64 = 0;
12941            for a in args.iter() {
12942                mask <<= 1;
12943                if dropped.iter().any(|d| d == a) {
12944                    mask |= 1;
12945                }
12946            }
12947            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12948            // literal: a bare integer in a select item is indistinguishable
12949            // from a positional reference once `ORDER BY 1` substitutes the
12950            // item back in, and the round-232 position check then read the
12951            // mask value as an out-of-range position. The cast changes
12952            // nothing semantically (grouping() is integer).
12953            *expr = Expr::Cast {
12954                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12955                target: crate::ast::CastTarget::Int,
12956            };
12957            return;
12958        }
12959        // Generic recursion over the common expression shapes the
12960        // SELECT list uses; anything without child expressions is
12961        // left alone.
12962        match expr {
12963            Expr::FunctionCall { args, .. } => {
12964                for a in args {
12965                    Self::substitute_grouping_calls(a, dropped);
12966                }
12967            }
12968            Expr::Binary { lhs, rhs, .. } => {
12969                Self::substitute_grouping_calls(lhs, dropped);
12970                Self::substitute_grouping_calls(rhs, dropped);
12971            }
12972            Expr::Unary { expr: inner, .. } => {
12973                Self::substitute_grouping_calls(inner, dropped);
12974            }
12975            Expr::Cast { expr: inner, .. } => {
12976                Self::substitute_grouping_calls(inner, dropped);
12977            }
12978            Expr::Case {
12979                operand,
12980                branches,
12981                else_branch,
12982            } => {
12983                if let Some(op) = operand {
12984                    Self::substitute_grouping_calls(op, dropped);
12985                }
12986                for (w, t) in branches {
12987                    Self::substitute_grouping_calls(w, dropped);
12988                    Self::substitute_grouping_calls(t, dropped);
12989                }
12990                if let Some(e) = else_branch {
12991                    Self::substitute_grouping_calls(e, dropped);
12992                }
12993            }
12994            // v7.38 (read01) — recurse into the remaining child-bearing shapes
12995            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
12996            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
12997            // …` is the canonical rollup-total label idiom).
12998            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
12999            Expr::Like { expr, pattern, .. } => {
13000                Self::substitute_grouping_calls(expr, dropped);
13001                Self::substitute_grouping_calls(pattern, dropped);
13002            }
13003            Expr::InList { expr, list, .. } => {
13004                Self::substitute_grouping_calls(expr, dropped);
13005                for item in list {
13006                    Self::substitute_grouping_calls(item, dropped);
13007                }
13008            }
13009            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13010            Expr::Array(items) => {
13011                for item in items {
13012                    Self::substitute_grouping_calls(item, dropped);
13013                }
13014            }
13015            Expr::ArraySubscript { target, index } => {
13016                Self::substitute_grouping_calls(target, dropped);
13017                Self::substitute_grouping_calls(index, dropped);
13018            }
13019            Expr::ArraySlice { target, lo, hi } => {
13020                Self::substitute_grouping_calls(target, dropped);
13021                if let Some(lo) = lo {
13022                    Self::substitute_grouping_calls(lo, dropped);
13023                }
13024                if let Some(hi) = hi {
13025                    Self::substitute_grouping_calls(hi, dropped);
13026                }
13027            }
13028            Expr::AnyAll { expr, array, .. } => {
13029                Self::substitute_grouping_calls(expr, dropped);
13030                Self::substitute_grouping_calls(array, dropped);
13031            }
13032            _ => {}
13033        }
13034    }
13035
13036    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13037        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13038        // group: `( <select chain> )` usable anywhere a query block
13039        // is (head or peer of an outer chain). The group's own
13040        // unions ride the returned SelectStatement; the executor's
13041        // nested-peer recursion runs them.
13042        if matches!(self.peek(), Token::LParen)
13043            && matches!(
13044                self.tokens.get(self.pos + 1),
13045                Some(Token::Select | Token::LParen | Token::Values)
13046            )
13047        {
13048            self.advance(); // (
13049            self.enter_nested()?;
13050            // v7.37 D.20 — a group whose head is a VALUES list:
13051            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13052            // otherwise recurse into a nested SELECT/group head.
13053            let mut head = (if matches!(self.peek(), Token::Values) {
13054                self.advance(); // VALUES
13055                self.parse_values_rows_body()
13056            } else {
13057                self.parse_bare_select()
13058            })
13059            .and_then(|mut h| {
13060                self.parse_setop_chain_into(&mut h)?;
13061                Ok(h)
13062            });
13063            self.nest_depth -= 1;
13064            let mut head = match &mut head {
13065                Ok(h) => core::mem::take(h),
13066                Err(_) => return head,
13067            };
13068            // v7.37.17 (17.6 siblings) — group-internal tail:
13069            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13070            // group head, then wrap the group as a derived table
13071            // (SELECT * FROM (group)) so the outer chain / outer
13072            // tail can't clobber the group's own ordering or limit.
13073            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13074                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13075                    if s.eq_ignore_ascii_case("fetch"));
13076            if has_tail {
13077                self.parse_select_tail_into(&mut head)?;
13078                head = SelectStatement {
13079                    locking: None,
13080                    ctes: Vec::new(),
13081                    distinct: false,
13082                    distinct_on: Vec::new(),
13083                    items: alloc::vec![SelectItem::Wildcard],
13084                    from: Some(FromClause {
13085                        primary: TableRef {
13086                            name: "subquery".to_string(),
13087                            alias: None,
13088                            only: false,
13089                            as_of_segment: None,
13090                            unnest_expr: None,
13091                            unnest_column_aliases: Vec::new(),
13092                            with_ordinality: false,
13093                            generate_series_args: None,
13094                            lateral_subquery: Some(Box::new(head)),
13095                            jsonb_each_text_arg: None,
13096                            table_fn_call: None,
13097                            rows_from: None,
13098                            json_table: None,
13099                            scalar_fn_item: false,
13100                        },
13101                        joins: Vec::new(),
13102                    }),
13103                    where_: None,
13104                    group_by: None,
13105                    group_by_all: false,
13106                    having: None,
13107                    unions: Vec::new(),
13108                    order_by: Vec::new(),
13109                    limit: None,
13110                    offset: None,
13111                    limit_with_ties: false,
13112                    window_check_exprs: Vec::new(),
13113                };
13114            }
13115            if !matches!(self.peek(), Token::RParen) {
13116                return Err(self.err(format!(
13117                    "expected ')' after parenthesized query group, got {:?}",
13118                    self.peek()
13119                )));
13120            }
13121            self.advance();
13122            return Ok(head);
13123        }
13124        // `TABLE name` shorthand as a query block — valid anywhere
13125        // a SELECT head is (set-op peers included).
13126        if matches!(self.peek(), Token::Table)
13127            && matches!(
13128                self.tokens.get(self.pos + 1),
13129                Some(Token::Ident(_) | Token::QuotedIdent(_))
13130            )
13131        {
13132            return self.parse_table_shorthand();
13133        }
13134        if !matches!(self.peek(), Token::Select) {
13135            return Err(self.err(format!(
13136                "expected SELECT to start a query block, got {:?}",
13137                self.peek()
13138            )));
13139        }
13140        self.advance();
13141        let distinct = if matches!(self.peek(), Token::Distinct) {
13142            self.advance();
13143            true
13144        } else {
13145            false
13146        };
13147        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13148        // keep the first row (per ORDER BY) of each group the
13149        // expressions define. Django's .distinct('field') shape.
13150        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13151            self.advance(); // ON
13152            if !matches!(self.peek(), Token::LParen) {
13153                return Err(self.err(format!(
13154                    "expected '(' after DISTINCT ON, got {:?}",
13155                    self.peek()
13156                )));
13157            }
13158            self.advance();
13159            let mut exprs = Vec::new();
13160            loop {
13161                exprs.push(self.parse_expr(0)?);
13162                match self.peek() {
13163                    Token::Comma => {
13164                        self.advance();
13165                    }
13166                    Token::RParen => break,
13167                    other => {
13168                        return Err(self.err(format!(
13169                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13170                        )));
13171                    }
13172                }
13173            }
13174            self.advance(); // )
13175            exprs
13176        } else {
13177            Vec::new()
13178        };
13179        let mut items = self.parse_select_list()?;
13180        // Scope the TABLESAMPLE lowering channel to this SELECT:
13181        // stash whatever an enclosing select accumulated, collect
13182        // our own FROM's predicates, restore after the combine.
13183        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13184        let mut from = if matches!(self.peek(), Token::From) {
13185            self.advance();
13186            Some(self.parse_from_clause()?)
13187        } else {
13188            None
13189        };
13190        // v7.37 D.22 — a set-returning function in the projection with no FROM
13191        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13192        // rows. Move the first SRF projection item to a FROM-position derived
13193        // table and replace it in the projection with a reference to its output
13194        // column; sibling scalar columns repeat per SRF row. PG names the output
13195        // column after the function (or its AS alias). Reuses the FROM-SRF
13196        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13197        // works via the targetlist-SRF path.
13198        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13199        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13200        // is exactly what the function's own row shape already is. Anywhere else
13201        // (per outer row, or beside other items) it would need a real record-typed
13202        // projection, so it says so rather than answering something else.
13203        if let [
13204            SelectItem::Expr {
13205                expr: Expr::FunctionCall { name, args },
13206                ..
13207            },
13208        ] = items.as_slice()
13209            && name == "__record_expand"
13210        {
13211            let Some(Expr::FunctionCall {
13212                name: inner_name,
13213                args: inner_args,
13214            }) = args.first()
13215            else {
13216                return Err(self.err(
13217                    "(<expr>).* expands a function's record — it needs a function call".into(),
13218                ));
13219            };
13220            if from.is_some() {
13221                return Err(self.err(
13222                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13223                        .into(),
13224                ));
13225            }
13226            let fn_ref = TableRef {
13227                name: inner_name.clone(),
13228                alias: None,
13229                only: false,
13230                as_of_segment: None,
13231                unnest_expr: None,
13232                unnest_column_aliases: Vec::new(),
13233                with_ordinality: false,
13234                generate_series_args: None,
13235                lateral_subquery: None,
13236                jsonb_each_text_arg: None,
13237                table_fn_call: Some(Box::new((
13238                    inner_name.to_ascii_lowercase(),
13239                    inner_args.clone(),
13240                ))),
13241                rows_from: None,
13242                json_table: None,
13243                scalar_fn_item: false,
13244            };
13245            items = alloc::vec![SelectItem::Wildcard];
13246            from = Some(FromClause {
13247                primary: fn_ref,
13248                joins: Vec::new(),
13249            });
13250        }
13251        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13252        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13253        // record's fields takes the catalog. It becomes a LATERAL of the same
13254        // function plus one item per declared column — the machinery rounds 65
13255        // and 69 already built.
13256        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13257        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13258        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13259        // express, since the lifted one becomes a scan and the other would
13260        // expand per its rows (a cross product, not a zip). So when the
13261        // projection holds more than one top-level function call, the lift steps
13262        // aside and the engine's target-list expansion takes the whole list.
13263        let fn_call_items = items
13264            .iter()
13265            .filter(|it| {
13266                matches!(
13267                    it,
13268                    SelectItem::Expr {
13269                        expr: Expr::FunctionCall { .. },
13270                        ..
13271                    }
13272                )
13273            })
13274            .count();
13275        if from.is_none() && fn_call_items <= 1 {
13276            let mut found: Option<(usize, TableRef, String)> = None;
13277            for (i, item) in items.iter().enumerate() {
13278                if let SelectItem::Expr {
13279                    expr: Expr::FunctionCall { name, args },
13280                    alias,
13281                } = item
13282                {
13283                    let lname = name.to_ascii_lowercase();
13284                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13285                    let (unnest, gs) = match lname.as_str() {
13286                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13287                        "generate_series" if (2..=3).contains(&args.len()) => {
13288                            (None, Some(args.clone()))
13289                        }
13290                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13291                        // no-FROM projection yields the 1-based subscripts, i.e.
13292                        // generate_series(1, array_length(arr, dim)); an invalid
13293                        // dimension makes array_length NULL → 0 rows, as in PG.
13294                        "generate_subscripts" if args.len() == 2 => (
13295                            None,
13296                            Some(alloc::vec![
13297                                Expr::Literal(Literal::Integer(1)),
13298                                Expr::FunctionCall {
13299                                    name: "array_length".to_string(),
13300                                    args: args.clone(),
13301                                },
13302                            ]),
13303                        ),
13304                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13305                        // in a no-FROM projection unnest their *_to_array form.
13306                        "string_to_table" | "regexp_split_to_table" => {
13307                            let array_fn = if lname == "string_to_table" {
13308                                "string_to_array"
13309                            } else {
13310                                "regexp_split_to_array"
13311                            };
13312                            (
13313                                Some(Box::new(Expr::FunctionCall {
13314                                    name: array_fn.to_string(),
13315                                    args: args.clone(),
13316                                })),
13317                                None,
13318                            )
13319                        }
13320                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13321                        // a no-FROM projection expand per element. The scalar form
13322                        // returns the elements as a TEXT array, so unnest over the
13323                        // same call materialises one row each (same rewrite the
13324                        // FROM-clause form uses).
13325                        "jsonb_array_elements"
13326                        | "json_array_elements"
13327                        | "jsonb_array_elements_text"
13328                        | "json_array_elements_text"
13329                            if args.len() == 1 =>
13330                        {
13331                            (
13332                                Some(Box::new(Expr::FunctionCall {
13333                                    name: lname.clone(),
13334                                    args: args.clone(),
13335                                })),
13336                                None,
13337                            )
13338                        }
13339                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13340                        // in a no-FROM projection expands per match (scalar form
13341                        // returns the matches as a TEXT array → unnest).
13342                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13343                            Some(Box::new(Expr::FunctionCall {
13344                                name: lname.clone(),
13345                                args: args.clone(),
13346                            })),
13347                            None,
13348                        ),
13349                        _ => continue,
13350                    };
13351                    found = Some((
13352                        i,
13353                        TableRef {
13354                            name: colname.clone(),
13355                            alias: Some(colname.clone()),
13356                            only: false,
13357                            as_of_segment: None,
13358                            unnest_expr: unnest,
13359                            unnest_column_aliases: alloc::vec![colname.clone()],
13360                            with_ordinality: false,
13361                            generate_series_args: gs,
13362                            lateral_subquery: None,
13363                            jsonb_each_text_arg: None,
13364                            table_fn_call: None,
13365                            rows_from: None,
13366                            json_table: None,
13367                            scalar_fn_item: false,
13368                        },
13369                        colname,
13370                    ));
13371                    break;
13372                }
13373            }
13374            if let Some((idx, tref, colname)) = found {
13375                from = Some(FromClause {
13376                    primary: tref,
13377                    joins: Vec::new(),
13378                });
13379                items[idx] = SelectItem::Expr {
13380                    expr: Expr::Column(ColumnName {
13381                        qualifier: None,
13382                        name: colname.clone(),
13383                    }),
13384                    alias: Some(colname),
13385                };
13386            }
13387        }
13388        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13389        let where_ = if matches!(self.peek(), Token::Where) {
13390            self.advance();
13391            Some(self.parse_expr(0)?)
13392        } else {
13393            None
13394        };
13395        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13396            Some(match acc {
13397                Some(w) => Expr::Binary {
13398                    lhs: Box::new(pred),
13399                    op: crate::ast::BinOp::And,
13400                    rhs: Box::new(w),
13401                },
13402                None => pred,
13403            })
13404        });
13405        self.pending_sample_preds = enclosing_sample_preds;
13406        let mut group_by_all = false;
13407        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13408        // share one expansion: `grouping_sets` lists the key subsets
13409        // (first = primary, assigned to stmt.group_by; the rest
13410        // become UNION ALL peers), `grouping_universe` is the full
13411        // key list used to compute each peer's dropped keys.
13412        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13413        let mut grouping_universe: Vec<Expr> = Vec::new();
13414        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13415        // A BOOL, not the key list: this frame is the statement parser's, and
13416        // round 430 measured that a `Vec` local here is enough on its own to
13417        // tip the 512 KiB nesting guard. The keys are recoverable from
13418        // `grouping_universe`, which a rollup fills with exactly them.
13419        let mut mysql_rollup = false;
13420        let group_by = if matches!(self.peek(), Token::Group) {
13421            self.advance();
13422            if !self.peek_is_by() {
13423                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13424            }
13425            self.advance();
13426            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13427            // every non-aggregate SELECT-list item later.
13428            if matches!(self.peek(), Token::All) {
13429                self.advance();
13430                group_by_all = true;
13431                None
13432            } else {
13433                // v7.39 (round 242) — PG's general grouping-element grammar:
13434                // GROUP BY [DISTINCT] element [, element]*, where an element
13435                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13436                // SETS (…) — mixed freely. Each element yields a list of
13437                // key sets; the query's grouping sets are the CARTESIAN
13438                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13439                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13440                // content. ROLLUP/CUBE members may be composite
13441                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13442                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13443                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13444                // clause.
13445                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13446                    self.advance();
13447                    true
13448                } else {
13449                    false
13450                };
13451                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13452                loop {
13453                    element_sets.push(self.parse_grouping_element()?);
13454                    if matches!(self.peek(), Token::Comma) {
13455                        self.advance();
13456                    } else {
13457                        break;
13458                    }
13459                }
13460                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13461                for el in &element_sets {
13462                    let mut next: Vec<Vec<Expr>> = Vec::new();
13463                    for base in &total {
13464                        for set in el {
13465                            let mut merged = base.clone();
13466                            for k in set {
13467                                if !merged.iter().any(|m| m == k) {
13468                                    merged.push(k.clone());
13469                                }
13470                            }
13471                            next.push(merged);
13472                        }
13473                    }
13474                    total = next;
13475                }
13476                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13477                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13478                // The keys and the aggregates come out identical; the ROW
13479                // ORDER does not, and that is the part a report depends on.
13480                // MySQL interleaves each group's subtotal right after its
13481                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13482                // where the union-of-grouping-sets expansion emits every
13483                // leaf first and then every subtotal. MariaDB REFUSES an
13484                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13485                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13486                // agree on the order and disagree only on whether ORDER BY
13487                // is allowed (MySQL allows it; SPG allows it too, since
13488                // refusing would break the clients that can write it).
13489                if self.mysql_dialect
13490                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13491                    && matches!(
13492                        self.tokens.get(self.pos + 1),
13493                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13494                    )
13495                {
13496                    self.advance(); // WITH
13497                    self.advance(); // ROLLUP
13498                    let keys = total.into_iter().next().unwrap_or_default();
13499                    mysql_rollup = true;
13500                    // n+1 prefixes, largest first — the same expansion
13501                    // `ROLLUP (…)` produces.
13502                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13503                }
13504                if distinct_sets {
13505                    let mut seen: Vec<Vec<String>> = Vec::new();
13506                    total.retain(|set| {
13507                        let mut key: Vec<String> =
13508                            set.iter().map(|e| alloc::format!("{e}")).collect();
13509                        key.sort();
13510                        if seen.contains(&key) {
13511                            false
13512                        } else {
13513                            seen.push(key);
13514                            true
13515                        }
13516                    });
13517                }
13518                if total.len() > 1 {
13519                    let mut universe: Vec<Expr> = Vec::new();
13520                    for set in &total {
13521                        for k in set {
13522                            if !universe.iter().any(|u| u == k) {
13523                                universe.push(k.clone());
13524                            }
13525                        }
13526                    }
13527                    grouping_universe = universe;
13528                    let primary = total[0].clone();
13529                    grouping_sets = total;
13530                    Some(primary)
13531                } else {
13532                    // One set (a plain GROUP BY list, or a single-set
13533                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13534                    // single set — GROUPING SETS (()) — stays
13535                    // `Some(vec![])`: the grand-total group, which must
13536                    // run the aggregate path.
13537                    Some(total.into_iter().next().unwrap_or_default())
13538                }
13539            }
13540        } else {
13541            None
13542        };
13543        let having = if matches!(self.peek(), Token::Having) {
13544            self.advance();
13545            Some(self.parse_expr(0)?)
13546        } else {
13547            None
13548        };
13549        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13550        // OVER w parsed to a marker above; inline each definition
13551        // into the referencing WindowFunction nodes.
13552        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13553        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13554            self.advance();
13555            loop {
13556                let wname = self.expect_ident_like()?;
13557                if !matches!(self.peek(), Token::As) {
13558                    return Err(self.err(format!(
13559                        "expected AS after WINDOW {wname}, got {:?}",
13560                        self.peek()
13561                    )));
13562                }
13563                self.advance();
13564                // v7.39 (round 229) — PG rejects a redefinition outright.
13565                if window_defs
13566                    .iter()
13567                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13568                {
13569                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13570                }
13571                let def = self.parse_over_clause()?;
13572                // A definition may itself copy an earlier one
13573                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13574                // so resolve it against the defs already in scope. Same
13575                // copy rules as an `OVER (w1 …)` in the select list.
13576                let mut probe = Expr::WindowFunction {
13577                    name: String::new(),
13578                    args: Vec::new(),
13579                    partition_by: def.0,
13580                    order_by: def.1,
13581                    frame: def.2,
13582                    null_treatment: crate::ast::NullTreatment::Respect,
13583                    filter: None,
13584                };
13585                Self::substitute_named_windows(&mut probe, &window_defs)
13586                    .map_err(|m| self.err(m))?;
13587                let Expr::WindowFunction {
13588                    partition_by,
13589                    order_by,
13590                    frame,
13591                    ..
13592                } = probe
13593                else {
13594                    unreachable!("probe is a WindowFunction")
13595                };
13596                window_defs.push((wname, (partition_by, order_by, frame)));
13597                if matches!(self.peek(), Token::Comma) {
13598                    self.advance();
13599                    continue;
13600                }
13601                break;
13602            }
13603        }
13604        // v7.39 (round 705) — which definitions did anything reference?
13605        // The ones nothing did used to be dropped here, unexamined, so
13606        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13607        // definition whether referenced or not. Their key expressions ride
13608        // out on the statement for the engine to resolve.
13609        let mut window_refs: Vec<String> = Vec::new();
13610        if !window_defs.is_empty() {
13611            for it in &items {
13612                if let SelectItem::Expr { expr, .. } = it {
13613                    Self::collect_named_window_refs(expr, &mut window_refs);
13614                }
13615            }
13616        }
13617        let window_check_exprs: Vec<Expr> = window_defs
13618            .iter()
13619            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13620            .flat_map(|(_, (partition, order, _))| {
13621                partition
13622                    .iter()
13623                    .cloned()
13624                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13625            })
13626            .collect();
13627        if !window_defs.is_empty()
13628            || items
13629                .iter()
13630                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13631        {
13632            for it in &mut items {
13633                if let SelectItem::Expr { expr, .. } = it {
13634                    Self::substitute_named_windows(expr, &window_defs)
13635                        .map_err(|m| self.err(m))?;
13636                }
13637            }
13638        }
13639        // `GROUP BY 1` — positional keys substitute with the Nth
13640        // select item's expression (same contract ORDER BY has had
13641        // since v6.x). Out-of-range positions error.
13642        let group_by = match group_by {
13643            Some(mut keys) => {
13644                for k in &mut keys {
13645                    if let Expr::Literal(Literal::Integer(n)) = k {
13646                        let idx = *n;
13647                        if idx < 1 || idx as usize > items.len() {
13648                            return Err(self.err(alloc::format!(
13649                                "GROUP BY position {idx} is not in select list"
13650                            )));
13651                        }
13652                        match &items[(idx - 1) as usize] {
13653                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13654                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13655                                return Err(self.err(alloc::format!(
13656                                    "GROUP BY position {idx} references a wildcard item"
13657                                )));
13658                            }
13659                        }
13660                    }
13661                }
13662                Some(keys)
13663            }
13664            None => None,
13665        };
13666        let mut stmt = SelectStatement {
13667            locking: None,
13668            ctes: Vec::new(),
13669            distinct,
13670            distinct_on,
13671            items,
13672            from,
13673            where_,
13674            group_by,
13675            group_by_all,
13676            having,
13677            unions: Vec::new(),
13678            order_by: Vec::new(),
13679            limit: None,
13680            offset: None,
13681            limit_with_ties: false,
13682            window_check_exprs,
13683        };
13684        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13685        // first set is the primary (already on stmt.group_by); each
13686        // further set becomes a UNION ALL peer with its dropped
13687        // keys (universe minus the set) replaced by NULL literals
13688        // in the peer's items and group_by. PG-legal: non-grouped
13689        // select items must be group keys or aggregates, so a
13690        // dropped key's occurrences in the projection are exactly
13691        // the ones to nullify.
13692        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13693        // over a plain GROUP BY (every argument must be a group key; the
13694        // mask is then 0) and rejects anything else with 42803. SPG's
13695        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13696        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13697        // function `grouping`".
13698        if grouping_sets.len() <= 1 {
13699            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13700            let mut calls: Vec<Expr> = Vec::new();
13701            for item in &stmt.items {
13702                if let SelectItem::Expr { expr, .. } = item {
13703                    Self::collect_grouping_calls(expr, &mut calls);
13704                }
13705            }
13706            if let Some(h) = &stmt.having {
13707                Self::collect_grouping_calls(h, &mut calls);
13708            }
13709            for call in &calls {
13710                let Expr::FunctionCall { args, .. } = call else {
13711                    continue;
13712                };
13713                for a in args {
13714                    if !keys.iter().any(|k| k == a) {
13715                        return Err(self.err(
13716                            "arguments to GROUPING must be grouping expressions of the associated query level"
13717                                .to_string(),
13718                        ));
13719                    }
13720                }
13721            }
13722            if !calls.is_empty() {
13723                for item in &mut stmt.items {
13724                    if let SelectItem::Expr { expr, .. } = item {
13725                        Self::substitute_grouping_calls(expr, &[]);
13726                    }
13727                }
13728                if let Some(h) = &mut stmt.having {
13729                    Self::substitute_grouping_calls(h, &[]);
13730                }
13731            }
13732        }
13733        if grouping_sets.len() > 1 {
13734            // The primary set's own dropped keys nullify in the
13735            // HEAD's projection too (GROUPING SETS's first set may
13736            // omit keys other sets use).
13737            let primary = grouping_sets[0].clone();
13738            let head_dropped: Vec<Expr> = grouping_universe
13739                .iter()
13740                .filter(|u| !primary.iter().any(|k| k == *u))
13741                .cloned()
13742                .collect();
13743            for set in grouping_sets.iter().skip(1) {
13744                let mut peer = stmt.clone();
13745                peer.unions = Vec::new();
13746                let dropped: Vec<&Expr> = grouping_universe
13747                    .iter()
13748                    .filter(|u| !set.iter().any(|k| k == *u))
13749                    .collect();
13750                // Empty set = grand-total group: `Some(vec![])` forces
13751                // the aggregate path (one collapsed row) instead of a
13752                // per-row passthrough. See the primary-set note above.
13753                peer.group_by = Some(set.clone());
13754                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13755                for item in &mut peer.items {
13756                    if let SelectItem::Expr { expr, alias } = item {
13757                        if dropped.iter().any(|d| *d == expr) {
13758                            // v7.39 — keep the dropped key's name on the
13759                            // NULL literal so the UNION output column
13760                            // (and any top-level ORDER BY on it) still
13761                            // resolves.
13762                            if alias.is_none()
13763                                && let Expr::Column(c) = &expr
13764                            {
13765                                *alias = Some(c.name.clone());
13766                            }
13767                            *expr = Expr::Literal(Literal::Null);
13768                        } else {
13769                            Self::substitute_grouping_calls(expr, &dropped_owned);
13770                        }
13771                    }
13772                }
13773                if let Some(h) = &mut peer.having {
13774                    Self::substitute_grouping_calls(h, &dropped_owned);
13775                }
13776                stmt.unions.push((UnionKind::All, peer));
13777            }
13778            for item in &mut stmt.items {
13779                if let SelectItem::Expr { expr, alias } = item {
13780                    if head_dropped.iter().any(|d| d == expr) {
13781                        if alias.is_none()
13782                            && let Expr::Column(c) = &expr
13783                        {
13784                            *alias = Some(c.name.clone());
13785                        }
13786                        *expr = Expr::Literal(Literal::Null);
13787                    } else {
13788                        Self::substitute_grouping_calls(expr, &head_dropped);
13789                    }
13790                }
13791            }
13792            if let Some(h) = &mut stmt.having {
13793                Self::substitute_grouping_calls(h, &head_dropped);
13794            }
13795            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13796            // (while `grouping_universe` / the per-branch sets are in scope). For
13797            // each grouping() call in it, inject a per-branch hidden column
13798            // `__grp_ord_K` carrying that branch's mask into the head + every
13799            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13800            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13801            // from the final output. A standalone grouping-set query has ORDER BY
13802            // (not an explicit set-op) next, so consuming it here is safe.
13803            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13804            // rollup carries the hierarchical order: sort by the grouping
13805            // keys with the rolled-up NULLs last, which is exactly the
13806            // interleaving both oracles emit. A client's own ORDER BY wins,
13807            // which is what MySQL does (MariaDB refuses to let one be
13808            // written at all).
13809            // The synthesised keys have to travel the SAME path a written
13810            // ORDER BY does: the block below is what turns a `grouping()`
13811            // call into the per-branch `__grp_ord_K` column the engine can
13812            // actually sort on. Bypassing it left a bare `grouping(text)`
13813            // for the evaluator to reject.
13814            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13815                self.parse_order_by_keys()?
13816            } else if mysql_rollup {
13817                Self::mysql_rollup_order(&grouping_universe)
13818            } else {
13819                Vec::new()
13820            };
13821            if !synthesised_or_parsed.is_empty() {
13822                let mut order_keys = synthesised_or_parsed;
13823                let mut grp_exprs: Vec<Expr> = Vec::new();
13824                for ob in &order_keys {
13825                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13826                }
13827                for (k, gexpr) in grp_exprs.iter().enumerate() {
13828                    let colname = alloc::format!("__grp_ord_{k}");
13829                    // Head branch (primary set) uses `head_dropped`.
13830                    let mut he = gexpr.clone();
13831                    Self::substitute_grouping_calls(&mut he, &head_dropped);
13832                    stmt.items.push(SelectItem::Expr {
13833                        expr: he,
13834                        alias: Some(colname.clone()),
13835                    });
13836                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13837                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13838                        let set = &grouping_sets[i + 1];
13839                        let dropped: Vec<Expr> = grouping_universe
13840                            .iter()
13841                            .filter(|u| !set.iter().any(|k| k == *u))
13842                            .cloned()
13843                            .collect();
13844                        let mut pe = gexpr.clone();
13845                        Self::substitute_grouping_calls(&mut pe, &dropped);
13846                        peer.items.push(SelectItem::Expr {
13847                            expr: pe,
13848                            alias: Some(colname.clone()),
13849                        });
13850                    }
13851                }
13852                for ob in &mut order_keys {
13853                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13854                }
13855                stmt.order_by = order_keys;
13856            }
13857        }
13858        Ok(stmt)
13859    }
13860
13861    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13862    /// as ORDER BY keys.
13863    ///
13864    /// Per key: the rollup marker, then the key. Sorting on the key alone
13865    /// is not enough, and a table with a NULL in it says why — MariaDB puts
13866    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13867    /// the ROLLUP-introduced NULL last, and both print as NULL.
13868    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13869    /// real group including the data-NULL one, 1 only for the row the
13870    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13871    /// rolls up to NULL|2, a|1, b|3, NULL|6.
13872    ///
13873    /// `#[inline(never)]`: its locals must not join the statement parser's
13874    /// frame, which round 430 measured sitting against the nesting guard.
13875    #[inline(never)]
13876    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13877        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13878        for e in keys {
13879            out.push(OrderBy {
13880                expr: Expr::FunctionCall {
13881                    name: "grouping".into(),
13882                    args: alloc::vec![e.clone()],
13883                },
13884                desc: false,
13885                nulls_first: None,
13886                collation: None,
13887            });
13888            out.push(OrderBy {
13889                expr: e.clone(),
13890                desc: false,
13891                // MySQL orders NULL first on an ascending key.
13892                nulls_first: Some(true),
13893                collation: None,
13894            });
13895        }
13896        out
13897    }
13898
13899    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13900    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13901    #[inline(never)]
13902    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13903        use crate::ast::MaintainKind;
13904        self.skip_paren_option_list();
13905        let kind = match self.peek() {
13906            // `TABLE` and `INDEX` lex as keywords, not identifiers.
13907            Token::Table | Token::Index => {
13908                self.advance();
13909                MaintainKind::ReindexRelation
13910            }
13911            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13912                "index" | "table" => {
13913                    self.advance();
13914                    MaintainKind::ReindexRelation
13915                }
13916                "schema" => {
13917                    self.advance();
13918                    MaintainKind::ReindexSchema
13919                }
13920                "system" | "database" => {
13921                    self.advance();
13922                    MaintainKind::Whole
13923                }
13924                // PG requires the object type; anything else is the
13925                // caller's problem, not something to swallow.
13926                _ => MaintainKind::ReindexRelation,
13927            },
13928            _ => MaintainKind::Whole,
13929        };
13930        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13931        // allows the plain form, so the modifier is recorded rather than
13932        // skipped. It still has no effect on how the reindex runs.
13933        let mut concurrently = false;
13934        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13935            self.advance();
13936            concurrently = true;
13937        }
13938        let target = self.take_optional_maintain_name();
13939        self.consume_until_statement_boundary();
13940        Ok(Statement::Maintain {
13941            kind,
13942            concurrently,
13943            target,
13944        })
13945    }
13946
13947    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13948    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13949    #[inline(never)]
13950    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13951        use crate::ast::MaintainKind;
13952        self.skip_paren_option_list();
13953        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13954            self.advance();
13955        }
13956        let target = self.take_optional_maintain_name();
13957        self.consume_until_statement_boundary();
13958        Ok(Statement::Maintain {
13959            kind: if target.is_some() {
13960                MaintainKind::ClusterRelation
13961            } else {
13962                MaintainKind::Whole
13963            },
13964            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13965            // transaction block quite happily (measured).
13966            concurrently: false,
13967            target,
13968        })
13969    }
13970
13971    /// The next token as a relation / schema name, when there is one.
13972    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13973        match self.peek() {
13974            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13975                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13976                _ => None,
13977            },
13978            _ => None,
13979        }
13980    }
13981
13982    /// A parenthesised option list, absorbed.
13983    fn skip_paren_option_list(&mut self) {
13984        if !matches!(self.peek(), Token::LParen) {
13985            return;
13986        }
13987        let mut depth = 0usize;
13988        loop {
13989            match self.advance() {
13990                Token::LParen => depth += 1,
13991                Token::RParen => {
13992                    depth -= 1;
13993                    if depth == 0 {
13994                        return;
13995                    }
13996                }
13997                Token::Eof => return,
13998                _ => {}
13999            }
14000        }
14001    }
14002
14003    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14004    /// column list.
14005    ///
14006    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14007    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14008    /// / ALL. The three that describe physical storage have no meaning
14009    /// here, so they parse and change nothing rather than making a
14010    /// dump that mentions them fail to load.
14011    ///
14012    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14013    /// parse chain the nesting sentinel is tuned against.
14014    #[inline(never)]
14015    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14016        self.advance(); // LIKE
14017        let source = self.expect_ident_like()?;
14018        let mut options = crate::ast::LikeOptions::default();
14019        loop {
14020            let including = match self.peek() {
14021                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14022                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14023                _ => break,
14024            };
14025            self.advance();
14026            // `ALL` lexes as its own keyword, not an identifier.
14027            let opt = if matches!(self.peek(), Token::All) {
14028                self.advance();
14029                alloc::string::String::from("all")
14030            } else {
14031                self.expect_ident_like()?
14032            };
14033            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14034                o.defaults = on;
14035                o.constraints = on;
14036                o.identity = on;
14037                o.generated = on;
14038                o.indexes = on;
14039                o.comments = on;
14040            };
14041            match opt.to_ascii_lowercase().as_str() {
14042                "all" => set(&mut options, including),
14043                "defaults" => options.defaults = including,
14044                "constraints" => options.constraints = including,
14045                "identity" => options.identity = including,
14046                "generated" => options.generated = including,
14047                "indexes" => options.indexes = including,
14048                "comments" => options.comments = including,
14049                // No storage model to copy into.
14050                "storage" | "statistics" | "compression" => {}
14051                other => {
14052                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14053                }
14054            }
14055        }
14056        Ok(crate::ast::LikeSpec {
14057            source,
14058            at,
14059            options,
14060        })
14061    }
14062
14063    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14064        // Caller already consumed CREATE; we're sitting on TABLE.
14065        debug_assert!(matches!(self.peek(), Token::Table));
14066        self.advance();
14067        let if_not_exists = self.consume_if_not_exists();
14068        let name = self.expect_ident_like()?;
14069        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14070        // child shape has no column list; the child inherits its
14071        // columns from the parent at engine-DDL time. Detect it
14072        // before the `(` requirement below.
14073        if matches!(self.peek(), Token::Partition)
14074            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14075        {
14076            self.advance(); // PARTITION
14077            self.advance(); // of
14078            let partition_of = self.parse_partition_of_tail()?;
14079            return Ok(Statement::CreateTable(CreateTableStatement {
14080                temporary: false,
14081                name,
14082                columns: Vec::new(),
14083                like_specs: Vec::new(),
14084                inherits: Vec::new(),
14085                if_not_exists,
14086                foreign_keys: Vec::new(),
14087                table_constraints: Vec::new(),
14088                partition_by: None,
14089                partition_of: Some(partition_of),
14090            }));
14091        }
14092        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14093        // the materialized-view materialisation path (run the SELECT, infer the
14094        // column types, create + populate the table) but marks the node so the
14095        // executor creates a plain table without a mat-view registry entry.
14096        if matches!(self.peek(), Token::As) {
14097            self.advance();
14098            let body_stmt = self.parse_select_stmt()?;
14099            let Statement::Select(body) = body_stmt else {
14100                return Err(self.err(format!(
14101                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14102                )));
14103            };
14104            let with_data = self.parse_optional_with_data(true)?;
14105            return Ok(Statement::CreateMaterializedView(
14106                crate::ast::CreateMaterializedViewStatement {
14107                    temporary: false,
14108                    name,
14109                    if_not_exists,
14110                    columns: Vec::new(),
14111                    body,
14112                    with_data,
14113                    as_plain_table: true,
14114                },
14115            ));
14116        }
14117        if !matches!(self.peek(), Token::LParen) {
14118            return Err(self.err(format!(
14119                "expected '(' after table name, got {:?}",
14120                self.peek()
14121            )));
14122        }
14123        self.advance();
14124        let mut columns = Vec::new();
14125        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14126        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14127        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14128        loop {
14129            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14130            // column list. It is how a child that adds nothing of its own is
14131            // written, and this loop demanded at least one entry: `syntax
14132            // error at or near ")"`. The child takes the parent's columns,
14133            // which the INHERITS clause already arranges.
14134            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14135                self.advance();
14136                break;
14137            }
14138            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14139            // clauses from column definitions. Constraints start
14140            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14141            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14142            // a column.
14143            if self.peek_table_level_pk_start() {
14144                table_constraints.push(self.parse_table_level_primary_key()?);
14145            } else if matches!(self.peek(), Token::Like) {
14146                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14147                // <opt> ]*`. The source table's shape lives in the catalog,
14148                // so this records the clause and the engine expands it.
14149                like_specs.push(self.parse_create_table_like(columns.len())?);
14150            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14151                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14152                table_constraints.push(self.parse_table_level_exclude()?);
14153            } else if self.peek_table_level_unique_start() {
14154                table_constraints.push(self.parse_table_level_unique()?);
14155            } else if self.peek_table_level_check_start() {
14156                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14157                table_constraints.push(self.parse_table_level_check()?);
14158            } else if self.peek_mysql_inline_key_start() {
14159                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14160                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14161                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14162                // inside the column list. Skip name + paren list;
14163                // for UNIQUE KEY, register as a UC.
14164                if let Some(uc) = self.parse_mysql_inline_key()? {
14165                    table_constraints.push(uc);
14166                }
14167            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14168                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14169                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14170                // CHECK is named, and the named-CONSTRAINT arm used
14171                // to accept FOREIGN KEY only. The name is accepted
14172                // and discarded — same handling as every other SPG
14173                // constraint name.
14174                self.advance(); // CONSTRAINT
14175                // v7.39 (read01 round 48) — the name is kept now: the schema
14176                // stores it, so DROP / RENAME CONSTRAINT can find it.
14177                let con_name = self.expect_ident_like()?;
14178                let mut tc = match kind {
14179                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14180                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14181                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14182                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14183                };
14184                match &mut tc {
14185                    crate::ast::TableConstraint::Check { name, .. }
14186                    | crate::ast::TableConstraint::Unique { name, .. }
14187                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14188                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14189                        *name = Some(con_name);
14190                    }
14191                    _ => {}
14192                }
14193                table_constraints.push(tc);
14194            } else if self.peek_constraint_or_fk_start() {
14195                foreign_keys.push(self.parse_table_level_fk()?);
14196            } else {
14197                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14198                // v7.13.0 — fold inline UNIQUE / CHECK column
14199                // constraints into table-level entries so the
14200                // engine path stays uniform.
14201                if col.is_unique {
14202                    table_constraints.push(crate::ast::TableConstraint::Unique {
14203                        name: None,
14204                        columns: alloc::vec![col.name.clone()],
14205                        nulls_not_distinct: col.unique_nulls_not_distinct,
14206                        deferrable: col.constraint_deferrable,
14207                        initially_deferred: col.constraint_initially_deferred,
14208                    });
14209                }
14210                if let Some(check_expr) = col.check.clone() {
14211                    table_constraints.push(crate::ast::TableConstraint::Check {
14212                        name: None,
14213                        expr: check_expr,
14214                        not_valid: false,
14215                    });
14216                }
14217                columns.push(col);
14218                if let Some(fk) = col_level_fk {
14219                    foreign_keys.push(fk);
14220                }
14221            }
14222            match self.peek() {
14223                Token::Comma => {
14224                    self.advance();
14225                }
14226                Token::RParen => {
14227                    self.advance();
14228                    break;
14229                }
14230                other => {
14231                    return Err(
14232                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14233                    );
14234                }
14235            }
14236        }
14237        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14238        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14239        // nothing is written between the parentheses.
14240        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14241        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14242        // empty parentheses were a parse error in their own right — quite apart
14243        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14244        // SPG does not have (filed separately).
14245        let _ = &like_specs;
14246        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14247        // It sits between the column list and the MySQL table options,
14248        // and it was a syntax error until this round.
14249        let mut inherits: Vec<String> = Vec::new();
14250        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14251            if k.eq_ignore_ascii_case("inherits"))
14252        {
14253            self.advance();
14254            if !matches!(self.peek(), Token::LParen) {
14255                return Err(self.err(alloc::format!(
14256                    "expected ( after INHERITS, got {:?}",
14257                    self.peek()
14258                )));
14259            }
14260            self.advance();
14261            loop {
14262                inherits.push(self.expect_ident_like()?);
14263                if matches!(self.peek(), Token::Comma) {
14264                    self.advance();
14265                    continue;
14266                }
14267                break;
14268            }
14269            if !matches!(self.peek(), Token::RParen) {
14270                return Err(self.err(alloc::format!(
14271                    "expected ) closing INHERITS, got {:?}",
14272                    self.peek()
14273                )));
14274            }
14275            self.advance();
14276        }
14277        // v7.14.0 — consume MySQL/MariaDB table options after the
14278        // closing `)`. mysqldump emits things like
14279        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14280        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14281        // SPG accepts all forms as no-ops (each option is
14282        // `<ident> [=] <ident-or-string>` separated by whitespace).
14283        self.consume_mysql_table_options();
14284        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14285        // SPG has no per-table reloptions, so accept and ignore them so a
14286        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14287        self.consume_with_reloptions();
14288        // v7.37.6-B — declarative-partition-parent suffix
14289        // (`PARTITION BY RANGE (key_col)`) sits after the column
14290        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14291        // and locks the key column at one ident; the engine then
14292        // verifies the column type is TIMESTAMPTZ.
14293        let partition_by = if matches!(self.peek(), Token::Partition) {
14294            self.advance(); // PARTITION
14295            if !self.peek_is_by() {
14296                return Err(self.err(format!(
14297                    "expected BY after PARTITION, got {:?}",
14298                    self.peek()
14299                )));
14300            }
14301            self.advance();
14302            Some(self.parse_partition_by_tail()?)
14303        } else {
14304            None
14305        };
14306        Ok(Statement::CreateTable(CreateTableStatement {
14307            temporary: false,
14308            name,
14309            columns,
14310            like_specs,
14311            inherits,
14312            if_not_exists,
14313            foreign_keys,
14314            table_constraints,
14315            partition_by,
14316            partition_of: None,
14317        }))
14318    }
14319
14320    /// v7.37.6-B — case-insensitive ident match helper for the
14321    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14322    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14323    /// didn't burn a global keyword slot for each (see the
14324    /// `Token::Partition` doc-comment in `lexer.rs`).
14325    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14326        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14327    }
14328
14329    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14330    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14331    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14332        use crate::ast::{PartitionBySpec, PartitionKindAst};
14333        let kind = match self.peek() {
14334            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14335                self.advance();
14336                PartitionKindAst::Range
14337            }
14338            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14339                self.advance();
14340                PartitionKindAst::List
14341            }
14342            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14343                self.advance();
14344                PartitionKindAst::Hash
14345            }
14346            other => {
14347                return Err(self.err(format!(
14348                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14349                )));
14350            }
14351        };
14352        if !matches!(self.peek(), Token::LParen) {
14353            return Err(self.err(format!(
14354                "expected '(' after PARTITION BY <strategy>, got {:?}",
14355                self.peek()
14356            )));
14357        }
14358        self.advance();
14359        let mut key_columns = Vec::new();
14360        loop {
14361            key_columns.push(self.expect_ident_like()?);
14362            match self.peek() {
14363                Token::Comma => {
14364                    self.advance();
14365                }
14366                Token::RParen => {
14367                    self.advance();
14368                    break;
14369                }
14370                other => {
14371                    return Err(self.err(format!(
14372                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14373                    )));
14374                }
14375            }
14376        }
14377        if key_columns.is_empty() {
14378            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14379        }
14380        Ok(PartitionBySpec { kind, key_columns })
14381    }
14382
14383    /// v7.37.6-B — after `PARTITION OF`, expect
14384    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14385    /// or
14386    ///   <parent> DEFAULT
14387    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14388        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14389        let parent_name = self.expect_ident_like()?;
14390        // v7.37.6-B rejects an explicit column list — the child
14391        // inherits from the parent. mailrs round-7 taught us that
14392        // CREATE TABLE-side schema reconciliation hides drift, so
14393        // we surface this as a parse error rather than silently
14394        // ignoring user columns.
14395        if matches!(self.peek(), Token::LParen) {
14396            return Err(self.err(
14397                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14398                 at v7.37.6-B; the child inherits its columns from the parent"
14399                    .to_string(),
14400            ));
14401        }
14402        let bounds = match self.peek() {
14403            Token::Default => {
14404                self.advance();
14405                PartitionOfBoundsAst::Default
14406            }
14407            Token::For => {
14408                self.advance();
14409                if !matches!(self.peek(), Token::Values) {
14410                    return Err(
14411                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14412                    );
14413                }
14414                self.advance();
14415                // WITH is not a reserved Token in the lexer — it lexes
14416                // as Token::Ident("with"). Disambiguate manually.
14417                let want_with = matches!(
14418                    self.peek(),
14419                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14420                );
14421                if want_with {
14422                    self.advance();
14423                    if !matches!(self.peek(), Token::LParen) {
14424                        return Err(self.err(format!(
14425                            "expected '(' after FOR VALUES WITH, got {:?}",
14426                            self.peek()
14427                        )));
14428                    }
14429                    self.advance();
14430                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14431                    loop {
14432                        let key = self.expect_ident_like()?;
14433                        let n = match self.peek().clone() {
14434                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14435                                self.advance();
14436                                v as u32
14437                            }
14438                            other => {
14439                                return Err(self.err(format!(
14440                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14441                                )));
14442                            }
14443                        };
14444                        match key.to_ascii_uppercase().as_str() {
14445                            "MODULUS" => modulus = Some(n),
14446                            "REMAINDER" => remainder = Some(n),
14447                            other => {
14448                                return Err(self.err(format!(
14449                                    "FOR VALUES WITH: unknown key {other:?}; \
14450                                     expected MODULUS or REMAINDER"
14451                                )));
14452                            }
14453                        }
14454                        match self.peek() {
14455                            Token::Comma => {
14456                                self.advance();
14457                            }
14458                            Token::RParen => {
14459                                self.advance();
14460                                break;
14461                            }
14462                            other => {
14463                                return Err(self.err(format!(
14464                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14465                                )));
14466                            }
14467                        }
14468                    }
14469                    let modulus = modulus
14470                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14471                    let remainder = remainder.ok_or_else(|| {
14472                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14473                    })?;
14474                    if modulus == 0 {
14475                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14476                    }
14477                    if remainder >= modulus {
14478                        return Err(self.err(format!(
14479                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14480                             must be < MODULUS ({modulus})"
14481                        )));
14482                    }
14483                    PartitionOfBoundsAst::Hash { modulus, remainder }
14484                } else {
14485                    match self.peek() {
14486                        Token::From => {
14487                            self.advance();
14488                            let lower = Box::new(self.parse_partition_bound_expr()?);
14489                            if !matches!(self.peek(), Token::To) {
14490                                return Err(self.err(format!(
14491                                    "expected TO after FROM (...), got {:?}",
14492                                    self.peek()
14493                                )));
14494                            }
14495                            self.advance();
14496                            let upper = Box::new(self.parse_partition_bound_expr()?);
14497                            PartitionOfBoundsAst::Range { lower, upper }
14498                        }
14499                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14500                        Token::In => {
14501                            self.advance();
14502                            if !matches!(self.peek(), Token::LParen) {
14503                                return Err(self.err(format!(
14504                                    "expected '(' after FOR VALUES IN, got {:?}",
14505                                    self.peek()
14506                                )));
14507                            }
14508                            self.advance();
14509                            let mut values = Vec::new();
14510                            loop {
14511                                values.push(self.parse_expr(0)?);
14512                                match self.peek() {
14513                                    Token::Comma => {
14514                                        self.advance();
14515                                    }
14516                                    Token::RParen => {
14517                                        self.advance();
14518                                        break;
14519                                    }
14520                                    other => {
14521                                        return Err(self.err(format!(
14522                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14523                                    )));
14524                                    }
14525                                }
14526                            }
14527                            if values.is_empty() {
14528                                return Err(self.err(
14529                                    "FOR VALUES IN requires at least one literal".to_string(),
14530                                ));
14531                            }
14532                            PartitionOfBoundsAst::List { values }
14533                        }
14534                        other => {
14535                            return Err(self.err(format!(
14536                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14537                            )));
14538                        }
14539                    }
14540                }
14541            }
14542            other => {
14543                return Err(self.err(format!(
14544                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14545                )));
14546            }
14547        };
14548        Ok(PartitionOfSpec {
14549            parent_name,
14550            bounds,
14551        })
14552    }
14553
14554    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14555    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14556    /// markers (no-arg builtins) so the engine resolves them
14557    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14558    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14559        if !matches!(self.peek(), Token::LParen) {
14560            return Err(self.err(format!(
14561                "expected '(' before partition bound, got {:?}",
14562                self.peek()
14563            )));
14564        }
14565        self.advance();
14566        let expr = match self.peek() {
14567            Token::Ident(s) | Token::QuotedIdent(s)
14568                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14569            {
14570                let name = s.to_ascii_uppercase();
14571                self.advance();
14572                crate::ast::Expr::FunctionCall {
14573                    name,
14574                    args: Vec::new(),
14575                }
14576            }
14577            _ => self.parse_expr(0)?,
14578        };
14579        if !matches!(self.peek(), Token::RParen) {
14580            return Err(self.err(format!(
14581                "expected ')' after partition bound, got {:?}",
14582                self.peek()
14583            )));
14584        }
14585        self.advance();
14586        Ok(expr)
14587    }
14588
14589    /// v7.14.0 — true when the next tokens look like an inline
14590    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14591    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14592    /// — each followed by an optional name + `(...)`. Critical:
14593    /// a column NAMED `key` / `index` (PG accepts as ident) must
14594    /// NOT be mistaken for the KEY constraint shape. We disambig
14595    /// by requiring the keyword to be followed by either `(` or
14596    /// `<ident> (`.
14597    fn peek_mysql_inline_key_start(&self) -> bool {
14598        let cur = self.peek();
14599        // Shapes:
14600        //   KEY (cols)
14601        //   KEY name (cols)
14602        //   INDEX (cols)
14603        //   INDEX name (cols)
14604        //   UNIQUE KEY [name] (cols)
14605        //   UNIQUE INDEX [name] (cols)
14606        //   FULLTEXT [KEY|INDEX] [name] (cols)
14607        //   SPATIAL [KEY|INDEX] [name] (cols)
14608        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14609            // tokens at skip = the position AFTER the index-form
14610            // keywords (KEY/INDEX) have been consumed.
14611            match self.tokens.get(skip) {
14612                Some(Token::LParen) => true,
14613                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14614                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14615                }
14616                _ => false,
14617            }
14618        };
14619        // `INDEX` lexes as Token::Index (reserved), not as
14620        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14621        // start; the peek helper below handles either.
14622        let is_key_or_index_tok = |t: &Token| -> bool {
14623            matches!(t, Token::Index)
14624                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14625        };
14626        match cur {
14627            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14628            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14629                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14630            }
14631            Token::Ident(s)
14632                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14633            {
14634                let nxt = self.tokens.get(self.pos + 1);
14635                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14636                    self.pos + 2
14637                } else {
14638                    self.pos + 1
14639                };
14640                after_keyword_followed_by_paren_or_ident_paren(after_after)
14641            }
14642            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14643                let nxt = self.tokens.get(self.pos + 1);
14644                if !nxt.is_some_and(is_key_or_index_tok) {
14645                    return false;
14646                }
14647                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14648            }
14649            _ => false,
14650        }
14651    }
14652
14653    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14654    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14655    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14656    /// returns Some(TableConstraint::Index) so the engine builds
14657    /// a real BTree index on the leading column (mysqldump
14658    /// `KEY idx_posts_author (author_id)` shape).
14659    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14660    /// (the storage layer has no matching AM).
14661    fn parse_mysql_inline_key(
14662        &mut self,
14663    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14664        // Detect UNIQUE prefix.
14665        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14666        {
14667            self.advance();
14668            true
14669        } else {
14670            false
14671        };
14672        // Consume FULLTEXT / SPATIAL prefix and record which one
14673        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14674        // dedicated TableConstraint variant so the engine can
14675        // build a tsvector-GIN; SPATIAL still has no matching
14676        // AM, so it falls back to accept-as-no-op.
14677        let mut is_fulltext = false;
14678        let mut is_spatial = false;
14679        if let Token::Ident(s) = self.peek().clone() {
14680            if s.eq_ignore_ascii_case("fulltext") {
14681                self.advance();
14682                is_fulltext = true;
14683            } else if s.eq_ignore_ascii_case("spatial") {
14684                self.advance();
14685                is_spatial = true;
14686            }
14687        }
14688        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14689        // (reserved); accept either token shape.
14690        match self.peek() {
14691            Token::Index => {
14692                self.advance();
14693            }
14694            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14695                self.advance();
14696            }
14697            other => {
14698                return Err(self.err(alloc::format!(
14699                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14700                )));
14701            }
14702        }
14703        // Optional index name (an ident before the `(`).
14704        // v7.15.0 — capture the name when present so the engine
14705        // builds the secondary index under the user's chosen
14706        // name (matches mysqldump's `KEY idx_x (col)` shape).
14707        let mut idx_name: Option<String> = None;
14708        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14709            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14710        {
14711            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14712                idx_name = Some(s);
14713            }
14714        }
14715        // Optional `USING BTREE` / `USING HASH` (MySQL).
14716        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14717            self.advance();
14718            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14719                self.advance();
14720            }
14721        }
14722        // Required column list `(col [, col]*)`.
14723        if !matches!(self.peek(), Token::LParen) {
14724            return Err(self.err(alloc::format!(
14725                "expected '(' in inline KEY/INDEX, got {:?}",
14726                self.peek()
14727            )));
14728        }
14729        self.advance();
14730        let mut cols: Vec<String> = Vec::new();
14731        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14732            self.advance();
14733            cols.push(s);
14734            // Skip optional `(length)` per-column prefix.
14735            if matches!(self.peek(), Token::LParen) {
14736                let mut depth = 1usize;
14737                self.advance();
14738                while depth > 0 {
14739                    match self.peek() {
14740                        Token::LParen => depth += 1,
14741                        Token::RParen => depth -= 1,
14742                        Token::Eof => break,
14743                        _ => {}
14744                    }
14745                    self.advance();
14746                }
14747            }
14748            // Skip optional ASC / DESC.
14749            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14750                || matches!(self.peek(), Token::Asc | Token::Desc)
14751            {
14752                self.advance();
14753            }
14754            if matches!(self.peek(), Token::Comma) {
14755                self.advance();
14756                continue;
14757            }
14758            break;
14759        }
14760        if matches!(self.peek(), Token::RParen) {
14761            self.advance();
14762        }
14763        // Trailing options on the inline index — comment / etc.
14764        // Skip until comma or `)`.
14765        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14766            self.advance();
14767        }
14768        if cols.is_empty() {
14769            return Ok(None);
14770        }
14771        if is_unique {
14772            // Carry the captured idx_name on UNIQUE too so future
14773            // engine work can name the underlying BTree
14774            // accordingly; today the unique-constraint installer
14775            // synthesises the name itself, but Display round-trip
14776            // benefits from preserving it.
14777            Ok(Some(crate::ast::TableConstraint::Unique {
14778                name: idx_name,
14779                columns: cols,
14780                nulls_not_distinct: false,
14781                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14782                deferrable: false,
14783                initially_deferred: false,
14784            }))
14785        } else if is_fulltext {
14786            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14787            // routes through `TableConstraint::FulltextIndex`;
14788            // the engine builds a tsvector-GIN over each named
14789            // column so MATCH AGAINST gets a real inverted
14790            // index instead of a silently-dropped declaration.
14791            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14792                name: idx_name,
14793                columns: cols,
14794            }))
14795        } else if is_spatial {
14796            // SPG has no native SPATIAL AM. Accept-as-no-op
14797            // (declaration is parsed, but no index is built).
14798            Ok(None)
14799        } else {
14800            // v7.15.0 — plain KEY / INDEX builds a real BTree
14801            // secondary index.
14802            Ok(Some(crate::ast::TableConstraint::Index {
14803                name: idx_name,
14804                columns: cols,
14805            }))
14806        }
14807    }
14808
14809    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14810    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14811    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14812    /// (in any order, separated by whitespace).
14813    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14814    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14815    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14816    /// bare ident here, and only the parenthesised form is reloptions (so this
14817    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14818    fn consume_with_reloptions(&mut self) {
14819        let is_with = matches!(
14820            self.peek(),
14821            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14822        );
14823        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14824            return;
14825        }
14826        self.advance(); // WITH
14827        self.advance(); // (
14828        let mut depth = 1u32;
14829        while depth > 0 && !matches!(self.peek(), Token::Eof) {
14830            match self.peek() {
14831                Token::LParen => depth += 1,
14832                Token::RParen => depth -= 1,
14833                _ => {}
14834            }
14835            self.advance();
14836        }
14837    }
14838
14839    fn consume_mysql_table_options(&mut self) {
14840        loop {
14841            // Heuristic: a table option is an ident (or `DEFAULT`
14842            // reserved keyword) followed by `=` and an
14843            // ident / string / integer.
14844            let name_lc = match self.peek().clone() {
14845                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14846                Token::Default => alloc::string::String::from("default"),
14847                _ => break,
14848            };
14849            let known = matches!(
14850                name_lc.as_str(),
14851                "engine"
14852                    | "default"
14853                    | "charset"
14854                    | "collate"
14855                    | "auto_increment"
14856                    | "row_format"
14857                    | "comment"
14858                    | "pack_keys"
14859                    | "stats_persistent"
14860                    | "stats_auto_recalc"
14861                    | "stats_sample_pages"
14862                    | "key_block_size"
14863                    | "tablespace"
14864                    | "min_rows"
14865                    | "max_rows"
14866                    | "checksum"
14867                    | "delay_key_write"
14868                    | "insert_method"
14869                    | "data"
14870                    | "index"
14871                    | "encryption"
14872                    | "compression"
14873            );
14874            if !known {
14875                break;
14876            }
14877            self.advance(); // option name
14878            // `DEFAULT` optional prefix is followed by `CHARSET` /
14879            // `COLLATE`; consume the next ident too.
14880            if name_lc == "default" {
14881                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14882                    self.advance();
14883                }
14884            }
14885            if matches!(self.peek(), Token::Eq) {
14886                self.advance();
14887            }
14888            match self.peek() {
14889                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14890                    self.advance();
14891                }
14892                _ => {}
14893            }
14894        }
14895    }
14896
14897    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14898    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14899    /// sure (otherwise a column literally named `primary` would
14900    /// be mistaken).
14901    fn peek_table_level_pk_start(&self) -> bool {
14902        let cur = self.peek();
14903        let nxt = self.tokens.get(self.pos + 1);
14904        let nxt2 = self.tokens.get(self.pos + 2);
14905        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14906        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14907        let is_lparen = matches!(nxt2, Some(Token::LParen));
14908        is_primary && is_key && is_lparen
14909    }
14910
14911    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14912    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14913    /// (mailrs round-5 G10).
14914    fn peek_table_level_unique_start(&self) -> bool {
14915        let cur = self.peek();
14916        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14917        if !is_unique {
14918            return false;
14919        }
14920        let n1 = self.tokens.get(self.pos + 1);
14921        // Plain `UNIQUE (…)`.
14922        if matches!(n1, Some(Token::LParen)) {
14923            return true;
14924        }
14925        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14926        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14927        if !is_nulls {
14928            return false;
14929        }
14930        let n2 = self.tokens.get(self.pos + 2);
14931        let n3 = self.tokens.get(self.pos + 3);
14932        let n4 = self.tokens.get(self.pos + 4);
14933        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14934        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14935            return true;
14936        }
14937        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14938        if matches!(n2, Some(Token::Not))
14939            && matches!(n3, Some(Token::Distinct))
14940            && matches!(n4, Some(Token::LParen))
14941        {
14942            return true;
14943        }
14944        false
14945    }
14946
14947    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14948        self.advance(); // PRIMARY
14949        self.advance(); // KEY
14950        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14951        // v7.39 (round 711) — the trailer's values are CARRIED now; round
14952        // 621 consumed and dropped them (the storing half of F08).
14953        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14954        Ok(crate::ast::TableConstraint::PrimaryKey {
14955            name: None,
14956            columns,
14957            deferrable,
14958            initially_deferred,
14959        })
14960    }
14961
14962    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14963        self.advance(); // UNIQUE
14964        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14965        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14966        // is `NULLS DISTINCT` per the SQL standard.
14967        let mut nulls_not_distinct = false;
14968        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14969            let n1 = self.tokens.get(self.pos + 1);
14970            let n2 = self.tokens.get(self.pos + 2);
14971            let is_not = matches!(n1, Some(Token::Not));
14972            let is_distinct = matches!(n2, Some(Token::Distinct));
14973            if is_not && is_distinct {
14974                self.advance(); // NULLS
14975                self.advance(); // NOT
14976                self.advance(); // DISTINCT
14977                nulls_not_distinct = true;
14978            } else if matches!(n1, Some(Token::Distinct)) {
14979                self.advance(); // NULLS
14980                self.advance(); // DISTINCT
14981            }
14982        }
14983        let columns = self.parse_paren_ident_list("UNIQUE")?;
14984        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14985        Ok(crate::ast::TableConstraint::Unique {
14986            name: None,
14987            columns,
14988            nulls_not_distinct,
14989            deferrable,
14990            initially_deferred,
14991        })
14992    }
14993
14994    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
14995    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
14996    /// expression.
14997    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
14998    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
14999    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15000    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15001    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15002    /// commit: `NOT` starts no other suffix here, but reading both
15003    /// tokens before advancing keeps the caller's error message intact
15004    /// if someone writes `NOT NULL` by mistake.
15005    fn parse_not_valid_suffix(&mut self) -> bool {
15006        if !matches!(self.peek(), Token::Not) {
15007            return false;
15008        }
15009        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15010        {
15011            return false;
15012        }
15013        self.advance();
15014        self.advance();
15015        true
15016    }
15017
15018    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15019        self.advance(); // EXCLUDE
15020        // Optional `USING <method>`.
15021        let mut method = None;
15022        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15023            self.advance();
15024            method = Some(match self.advance() {
15025                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15026                other => {
15027                    return Err(self.err(alloc::format!(
15028                        "expected index method after USING, got {other:?}"
15029                    )));
15030                }
15031            });
15032        }
15033        if !matches!(self.peek(), Token::LParen) {
15034            return Err(self.err(alloc::format!(
15035                "expected '(' after EXCLUDE, got {:?}",
15036                self.peek()
15037            )));
15038        }
15039        self.advance();
15040        let mut elements: Vec<(String, String)> = Vec::new();
15041        loop {
15042            let col = match self.advance() {
15043                Token::Ident(s) | Token::QuotedIdent(s) => s,
15044                other => {
15045                    return Err(self.err(alloc::format!(
15046                        "expected column name in EXCLUDE, got {other:?}"
15047                    )));
15048                }
15049            };
15050            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15051                return Err(self.err(alloc::format!(
15052                    "expected WITH after EXCLUDE column, got {:?}",
15053                    self.peek()
15054                )));
15055            }
15056            self.advance();
15057            let op = match self.advance() {
15058                Token::InetOverlap => String::from("&&"),
15059                Token::Intersects => String::from("?#"),
15060                Token::IsBelow => String::from("<^"),
15061                Token::IsAbove => String::from(">^"),
15062                Token::PatternLt => String::from("~<~"),
15063                Token::PatternLtEq => String::from("~<=~"),
15064                Token::PatternGt => String::from("~>~"),
15065                Token::PatternGtEq => String::from("~>=~"),
15066                Token::TsMatchOld => String::from("@@@"),
15067                Token::Eq => String::from("="),
15068                Token::JsonContains => String::from("@>"),
15069                Token::JsonContainedBy => String::from("<@"),
15070                Token::OverLeft => String::from("&<"),
15071                Token::OverRight => String::from("&>"),
15072                other => {
15073                    return Err(self.err(alloc::format!(
15074                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15075                    )));
15076                }
15077            };
15078            elements.push((col, op));
15079            if matches!(self.peek(), Token::Comma) {
15080                self.advance();
15081                continue;
15082            }
15083            break;
15084        }
15085        if !matches!(self.peek(), Token::RParen) {
15086            return Err(self.err(alloc::format!(
15087                "expected ')' to close EXCLUDE, got {:?}",
15088                self.peek()
15089            )));
15090        }
15091        self.advance();
15092        Ok(crate::ast::TableConstraint::Exclude {
15093            name: None,
15094            method,
15095            elements,
15096        })
15097    }
15098
15099    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15100        self.advance(); // CHECK
15101        if !matches!(self.peek(), Token::LParen) {
15102            return Err(self.err(alloc::format!(
15103                "expected '(' after CHECK, got {:?}",
15104                self.peek()
15105            )));
15106        }
15107        self.advance();
15108        let expr = self.parse_expr(0)?;
15109        if !matches!(self.peek(), Token::RParen) {
15110            return Err(self.err(alloc::format!(
15111                "expected ')' to close CHECK predicate, got {:?}",
15112                self.peek()
15113            )));
15114        }
15115        self.advance();
15116        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15117        // are no existing rows for PG to skip, so it rejects the suffix.
15118        Ok(crate::ast::TableConstraint::Check {
15119            name: None,
15120            expr,
15121            not_valid: false,
15122        })
15123    }
15124
15125    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15126    fn peek_table_level_check_start(&self) -> bool {
15127        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15128    }
15129
15130    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15131    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15132    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15133    /// own CONSTRAINT prefix).
15134    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15135        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15136            return None;
15137        }
15138        // tokens[pos+1] is the constraint name (any ident-like);
15139        // tokens[pos+2] is the kind keyword.
15140        match self.tokens.get(self.pos + 2) {
15141            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15142                Some(NamedTableConstraintKind::Check)
15143            }
15144            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15145                Some(NamedTableConstraintKind::Unique)
15146            }
15147            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15148                Some(NamedTableConstraintKind::PrimaryKey)
15149            }
15150            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15151                Some(NamedTableConstraintKind::Exclude)
15152            }
15153            _ => None,
15154        }
15155    }
15156
15157    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15158        if !matches!(self.peek(), Token::LParen) {
15159            return Err(self.err(alloc::format!(
15160                "expected '(' after {ctx}, got {:?}",
15161                self.peek()
15162            )));
15163        }
15164        self.advance();
15165        let mut out = Vec::new();
15166        loop {
15167            out.push(self.expect_ident_like()?);
15168            match self.peek() {
15169                Token::Comma => {
15170                    self.advance();
15171                }
15172                Token::RParen => {
15173                    self.advance();
15174                    break;
15175                }
15176                other => {
15177                    return Err(self.err(alloc::format!(
15178                        "expected ',' or ')' in {ctx} list, got {other:?}"
15179                    )));
15180                }
15181            }
15182        }
15183        if out.is_empty() {
15184            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15185        }
15186        Ok(out)
15187    }
15188
15189    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15190    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15191    /// table-level FK; a column def never starts with either keyword
15192    /// (column names are not in this reserved set).
15193    fn peek_constraint_or_fk_start(&self) -> bool {
15194        let is_constraint_kw = matches!(
15195            self.peek(),
15196            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15197        );
15198        let is_foreign_kw = matches!(
15199            self.peek(),
15200            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15201        );
15202        is_constraint_kw || is_foreign_kw
15203    }
15204
15205    /// v7.6.0 — parse a table-level FK clause:
15206    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15207    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15208    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15209        let mut name: Option<String> = None;
15210        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15211            self.advance();
15212            name = Some(self.expect_ident_like()?);
15213        }
15214        // `FOREIGN`
15215        match self.advance() {
15216            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15217            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15218        }
15219        // `KEY`
15220        match self.advance() {
15221            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15222            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15223        }
15224        // `(col, col, ...)`
15225        if !matches!(self.peek(), Token::LParen) {
15226            return Err(self.err(format!(
15227                "expected '(' after FOREIGN KEY, got {:?}",
15228                self.peek()
15229            )));
15230        }
15231        self.advance();
15232        let mut columns = Vec::new();
15233        loop {
15234            columns.push(self.expect_ident_like()?);
15235            match self.peek() {
15236                Token::Comma => {
15237                    self.advance();
15238                }
15239                Token::RParen => {
15240                    self.advance();
15241                    break;
15242                }
15243                other => {
15244                    return Err(self.err(format!(
15245                        "expected ',' or ')' in FK column list, got {other:?}"
15246                    )));
15247                }
15248            }
15249        }
15250        if columns.is_empty() {
15251            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15252        }
15253        let (
15254            parent_table,
15255            parent_columns,
15256            on_delete,
15257            on_update,
15258            match_type,
15259            deferrable,
15260            initially_deferred,
15261        ) = self.parse_references_tail(columns.len())?;
15262        Ok(ForeignKeyConstraint {
15263            name,
15264            columns,
15265            parent_table,
15266            parent_columns,
15267            on_delete,
15268            on_update,
15269            match_type,
15270            deferrable,
15271            initially_deferred,
15272        })
15273    }
15274
15275    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15276    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15277    /// the local column count, used to default the parent column
15278    /// list when omitted (SQL spec: parent's PK is implied).
15279    fn parse_references_tail(
15280        &mut self,
15281        expected_arity: usize,
15282    ) -> Result<
15283        (
15284            String,
15285            Vec<String>,
15286            FkAction,
15287            FkAction,
15288            crate::ast::MatchType,
15289            // v7.39 (round 288) — deferrable, initially_deferred.
15290            bool,
15291            bool,
15292        ),
15293        ParseError,
15294    > {
15295        match self.advance() {
15296            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15297            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15298        }
15299        let parent_table = self.expect_ident_like()?;
15300        let mut parent_columns: Vec<String> = Vec::new();
15301        if matches!(self.peek(), Token::LParen) {
15302            self.advance();
15303            loop {
15304                parent_columns.push(self.expect_ident_like()?);
15305                match self.peek() {
15306                    Token::Comma => {
15307                        self.advance();
15308                    }
15309                    Token::RParen => {
15310                        self.advance();
15311                        break;
15312                    }
15313                    other => {
15314                        return Err(self.err(format!(
15315                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15316                        )));
15317                    }
15318                }
15319            }
15320        }
15321        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15322            return Err(self.err(format!(
15323                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15324                expected_arity,
15325                parent_columns.len()
15326            )));
15327        }
15328        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15329        // it between the referenced column list and the ON / DEFERRABLE
15330        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15331        // is skipped when any referencing column is NULL), so SIMPLE —
15332        // the default, and the only spelling pg_dump emits — is accepted
15333        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15334        // mixed-NULL rule, which is not wired yet; reject them honestly
15335        // rather than silently applying SIMPLE (PG itself errors on
15336        // MATCH PARTIAL as "not yet implemented").
15337        let mut match_type = crate::ast::MatchType::Simple;
15338        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15339            self.advance();
15340            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15341            // SIMPLE / PARTIAL arrive as bare identifiers.
15342            let kind = match self.advance() {
15343                Token::Full => "FULL".to_string(),
15344                Token::Ident(s) => s.to_uppercase(),
15345                other => {
15346                    return Err(self.err(format!(
15347                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15348                    )));
15349                }
15350            };
15351            match kind.as_str() {
15352                "SIMPLE" => {} // Default — match_type stays Simple.
15353                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15354                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15355                "FULL" => match_type = crate::ast::MatchType::Full,
15356                "PARTIAL" => {
15357                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15358                }
15359                _ => {
15360                    return Err(self.err(format!(
15361                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15362                    )));
15363                }
15364            }
15365        }
15366        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15367        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15368        // <action>` / `ON UPDATE <action>` in either order. PG /
15369        // pg_dump emits the timing clause AFTER the ON clauses
15370        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15371        // but the SQL spec allows either order. We loop over
15372        // every possible trailer and dispatch on the next token,
15373        // stopping when nothing matches. Phase 3.1 changes the
15374        // bare DEFERRABLE form from hard-error to accept-as-
15375        // immediate; SPG is single-writer with no deferred-
15376        // constraint window so the runtime semantics are always
15377        // immediate even when INITIALLY DEFERRED is requested.
15378        // PG's default referential action (no ON DELETE / ON UPDATE
15379        // clause) is NO ACTION, not RESTRICT — the two enforce
15380        // identically in SPG (single-writer, no deferred window; see the
15381        // shared match arm in constraints.rs) but information_schema.
15382        // referential_constraints must report NO ACTION to match PG.
15383        let mut on_delete = FkAction::NoAction;
15384        let mut on_update = FkAction::NoAction;
15385        let mut seen_on_delete = false;
15386        let mut seen_on_update = false;
15387        let mut deferrable = false;
15388        let mut initially_deferred = false;
15389        loop {
15390            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15391            let before = self.pos;
15392            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15393            if self.pos != before {
15394                deferrable = d;
15395                initially_deferred = idef;
15396                continue;
15397            }
15398            // ON DELETE / ON UPDATE.
15399            if !matches!(self.peek(), Token::On) {
15400                break;
15401            }
15402            self.advance();
15403            let which = self.advance();
15404            let action = self.parse_fk_action()?;
15405            match which {
15406                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15407                    if seen_on_delete {
15408                        return Err(self.err("ON DELETE specified twice".into()));
15409                    }
15410                    seen_on_delete = true;
15411                    on_delete = action;
15412                }
15413                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15414                    if seen_on_update {
15415                        return Err(self.err("ON UPDATE specified twice".into()));
15416                    }
15417                    seen_on_update = true;
15418                    on_update = action;
15419                }
15420                other => {
15421                    return Err(
15422                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15423                    );
15424                }
15425            }
15426        }
15427        Ok((
15428            parent_table,
15429            parent_columns,
15430            on_delete,
15431            on_update,
15432            match_type,
15433            deferrable,
15434            initially_deferred,
15435        ))
15436    }
15437
15438    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15439    /// NO ACTION`.
15440    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15441        match self.advance() {
15442            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15443            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15444            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15445                Token::Null => Ok(FkAction::SetNull),
15446                Token::Default => Ok(FkAction::SetDefault),
15447                other => Err(self.err(format!(
15448                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15449                ))),
15450            },
15451            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15452                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15453                other => Err(self.err(format!(
15454                    "expected ACTION after NO in FK action, got {other:?}"
15455                ))),
15456            },
15457            other => Err(self.err(format!(
15458                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15459            ))),
15460        }
15461    }
15462
15463    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15464    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15465    fn consume_if_not_exists(&mut self) -> bool {
15466        // `IF` arrives as a bare Ident (we don't reserve it because it
15467        // also appears mid-expression in PG, though we don't support
15468        // those forms yet).
15469        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15470        if !looks_like_if {
15471            return false;
15472        }
15473        // Peek one ahead before committing: only consume IF when it's
15474        // actually `IF NOT EXISTS`.
15475        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15476            return false;
15477        }
15478        if !matches!(
15479            self.tokens.get(self.pos + 2),
15480            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15481        ) {
15482            return false;
15483        }
15484        self.advance(); // IF
15485        self.advance(); // NOT
15486        self.advance(); // EXISTS
15487        true
15488    }
15489
15490    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15491    /// Consumes IF EXISTS as a pair; returns false otherwise
15492    /// without consuming any tokens.
15493    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15494    /// ENABLE/DISABLE/FORCE/NO FORCE.
15495    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15496        for kw in ["row", "level", "security"] {
15497            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15498            {
15499                return Err(self.err(alloc::format!(
15500                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15501                    kw.to_ascii_uppercase(),
15502                    self.peek()
15503                )));
15504            }
15505            self.advance();
15506        }
15507        Ok(())
15508    }
15509
15510    fn consume_if_exists(&mut self) -> bool {
15511        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15512        if !looks_like_if {
15513            return false;
15514        }
15515        if !matches!(
15516            self.tokens.get(self.pos + 1),
15517            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15518        ) {
15519            return false;
15520        }
15521        self.advance(); // IF
15522        self.advance(); // EXISTS
15523        true
15524    }
15525
15526    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15527    /// qualifiers after an index column ref. ASC / DESC are
15528    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15529    /// We accept and discard them since single-column BTree
15530    /// stores rows in natural key order today.
15531    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15532    /// ORDER BY key. Returns None when absent.
15533    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15534        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15535            return Ok(None);
15536        }
15537        self.advance();
15538        match self.advance() {
15539            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15540            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15541            other => Err(self.err(alloc::format!(
15542                "expected FIRST or LAST after NULLS, got {other:?}"
15543            ))),
15544        }
15545    }
15546
15547    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15548    /// rather than discarded.
15549    ///
15550    /// SPG's index does not scan in a direction — column ordering is
15551    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15552    /// reproduction of the DDL, and dropping the clause meant
15553    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15554    /// dump lost it, and a schema diff saw drift on every run.
15555    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15556        let mut order = crate::ast::IndexColumnOrder::default();
15557        loop {
15558            match self.peek() {
15559                Token::Asc => {
15560                    self.advance();
15561                }
15562                Token::Desc => {
15563                    order.descending = true;
15564                    self.advance();
15565                }
15566                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15567                    let look = self.tokens.get(self.pos + 1);
15568                    if matches!(
15569                        look,
15570                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15571                            || k.eq_ignore_ascii_case("last")
15572                    ) {
15573                        self.advance();
15574                        order.nulls_first = Some(matches!(
15575                            self.advance(),
15576                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15577                        ));
15578                    } else {
15579                        break;
15580                    }
15581                }
15582                _ => break,
15583            }
15584        }
15585        order
15586    }
15587
15588    fn parse_create_index_stmt_after_create(
15589        &mut self,
15590        is_unique: bool,
15591    ) -> Result<Statement, ParseError> {
15592        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15593        debug_assert!(matches!(self.peek(), Token::Index));
15594        self.advance();
15595        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15596        // SPG's CREATE INDEX is synchronous end-to-end today (real
15597        // CONCURRENTLY variant with restartable scans queues with
15598        // v7.39 indexes epic), so the modifier has no runtime effect
15599        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15600        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15601        // VIEW CONCURRENTLY.
15602        let mut concurrently = false;
15603        if matches!(
15604            self.peek(),
15605            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15606        ) {
15607            self.advance();
15608            concurrently = true;
15609        }
15610        let if_not_exists = self.consume_if_not_exists();
15611        // v7.39 (read01 round 93) — the index name is optional (PG since
15612        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15613        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15614        // was given; leave it empty and the engine derives a PG-style
15615        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15616        let name = if matches!(self.peek(), Token::On) {
15617            String::new()
15618        } else {
15619            self.expect_ident_like()?
15620        };
15621        if !matches!(self.peek(), Token::On) {
15622            return Err(self.err(format!(
15623                "expected ON after CREATE INDEX <name>, got {:?}",
15624                self.peek()
15625            )));
15626        }
15627        self.advance();
15628        let table = self.expect_ident_like()?;
15629        // Optional `USING <method>` — only recognised method in v2.0 is
15630        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15631        // ident `using` (we don't promote it to a reserved keyword
15632        // because it isn't reserved anywhere else in our SQL surface).
15633        let mut method_name: Option<String> = None;
15634        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15635            self.advance();
15636            let m = self.expect_ident_like()?;
15637            method_name = Some(m.to_ascii_lowercase());
15638            match m.to_ascii_lowercase().as_str() {
15639                "hnsw" => IndexMethod::Hnsw,
15640                "btree" => IndexMethod::BTree,
15641                "brin" => IndexMethod::Brin,
15642                // v7.12.3 — real GIN inverted index over `tsvector`.
15643                // v7.9.26b's `USING gin` → BTree silent fallback is
15644                // gone; the engine validates that the indexed column
15645                // is `tsvector` at CREATE INDEX time.
15646                "gin" => IndexMethod::Gin,
15647                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15648                // `USING spgist` / `USING hash` for their built-in
15649                // AMs that SPG doesn't have a matching
15650                // implementation for; degrade to BTree on the
15651                // leading column so the schema loads + the index
15652                // catalogue stays consistent. Operator pays the
15653                // planner cost only for the queries that would have
15654                // used the specialised AM.
15655                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15656                // v7.11.3 — pgvector ships both `ivfflat` and
15657                // `hnsw`. Customers shouldn't have to choose
15658                // their on-disk index method based on what SPG
15659                // implements; accept `ivfflat` as a synonym for
15660                // `hnsw` so PG schemas using either method drop
15661                // in. The vector distance op (`<->` / `<#>` /
15662                // `<=>`) at query time still picks the metric.
15663                "ivfflat" => IndexMethod::Hnsw,
15664                other => {
15665                    return Err(self.err(alloc::format!(
15666                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15667                    )));
15668                }
15669            }
15670        } else {
15671            IndexMethod::BTree
15672        };
15673        if !matches!(self.peek(), Token::LParen) {
15674            return Err(self.err(format!(
15675                "expected '(' before indexed column, got {:?}",
15676                self.peek()
15677            )));
15678        }
15679        self.advance();
15680        // v6.8.2 — accept either a bare column ident (legacy) or
15681        // an expression `fn(col, …)` for expression indexes.
15682        // Distinguish by peeking the token *after* the current
15683        // ident: `ident )` is the legacy column-only path;
15684        // anything else triggers the Pratt expression parser.
15685        // (`advance()` uses `mem::replace` to nil out the current
15686        // slot, so we can't save+rewind cleanly — peek-ahead via
15687        // direct index avoids the mutation.)
15688        let mut opclass: Option<String> = None;
15689        let mut key_collation: Option<String> = None;
15690        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15691            // Single column with `)` immediately after — fast path.
15692            // v7.9.29 — also: bare column followed by `,` (the
15693            // multi-column form `(a, b, c)`). Without this branch
15694            // the leading ident gets pulled into `parse_expr`
15695            // which then sets `expression = Some(Column(a))` and
15696            // breaks Display round-trip on the multi-column shape.
15697            Token::Ident(s) | Token::QuotedIdent(s)
15698                if matches!(
15699                    self.tokens.get(self.pos + 1),
15700                    Some(Token::RParen | Token::Comma)
15701                ) =>
15702            {
15703                self.advance();
15704                (s, None)
15705            }
15706            // v7.9.22 — single column followed by a pgvector
15707            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15708            // v7.15.0 — capture the opclass instead of discarding
15709            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15710            // → real trigram-shingle GIN over a TEXT column).
15711            // Vector/HNSW opclasses still take their distance
15712            // metric from the query operator (`<->` / `<#>` /
15713            // `<=>`), so for those callers the opclass stays
15714            // informational.
15715            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15716            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15717            // the schema and dispatch on the bare opclass, the same
15718            // treatment table/type names get.
15719            Token::Ident(s) | Token::QuotedIdent(s)
15720                if matches!(
15721                    self.tokens.get(self.pos + 1),
15722                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15723                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15724                    && matches!(
15725                        self.tokens.get(self.pos + 3),
15726                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15727                            if is_vector_opclass_name(op)
15728                    ) =>
15729            {
15730                self.advance(); // column name
15731                self.advance(); // schema qualifier
15732                self.advance(); // dot
15733                let op_tok = self.advance();
15734                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15735                    opclass = Some(op.to_ascii_lowercase());
15736                }
15737                (s, None)
15738            }
15739            // r1038 — an operator class is recognised by its POSITION, not
15740            // by a list of names. It used to be `is_vector_opclass_name`,
15741            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15742            // sentori's migration wrote — was a syntax error while
15743            // `USING gin (doc)` parsed. Anything sitting between a column
15744            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15745            // two bare identifiers in a row are not valid there otherwise.
15746            Token::Ident(s) | Token::QuotedIdent(s)
15747                if matches!(
15748                    self.tokens.get(self.pos + 1),
15749                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15750                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
15751                            self.tokens.get(self.pos + 2)
15752                        )
15753                ) =>
15754            {
15755                self.advance(); // column name
15756                // Capture the opclass token, lower-cased for
15757                // case-insensitive engine dispatch.
15758                let op_tok = self.advance();
15759                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15760                    opclass = Some(op.to_ascii_lowercase());
15761                }
15762                (s, None)
15763            }
15764            Token::Ident(_) | Token::QuotedIdent(_) => {
15765                // v7.39 (round 538) — an explicit COLLATE on the key,
15766                // read by LOOKAHEAD because `parse_expr` absorbs the
15767                // clause as a no-op (SPG orders text by bytes, which is
15768                // the C collation, so it changes nothing to honour). PG
15769                // still PRINTS it: an explicitly written `"C"` and the
15770                // collation a column inherits are different collation
15771                // OBJECTS even where they sort identically, which is why
15772                // `(a COLLATE "C")` shows on a C-collation database too.
15773                if matches!(
15774                    self.tokens.get(self.pos + 1),
15775                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15776                ) {
15777                    key_collation = match self.tokens.get(self.pos + 2) {
15778                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15779                            Some(n.clone())
15780                        }
15781                        _ => None,
15782                    };
15783                }
15784                let key_expr = self.parse_expr(0)?;
15785                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15786                    self.err("expression index key must reference at least one column".into())
15787                })?;
15788                (primary, Some(key_expr))
15789            }
15790            // v7.37.43-T4 — parenthesised expression index key
15791            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15792            // PG's CREATE INDEX requires the expression to be in
15793            // its own parens to disambiguate function calls from
15794            // column lists, so this `LParen` is the inner open-paren
15795            // of an expression key. parse_expr handles the recursive
15796            // descent and consumes the matching `RParen`.
15797            Token::LParen => {
15798                let key_expr = self.parse_expr(0)?;
15799                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15800                    self.err("expression index key must reference at least one column".into())
15801                })?;
15802                (primary, Some(key_expr))
15803            }
15804            other => {
15805                return Err(self.err(format!(
15806                    "expected column ident or expression, got {other:?}"
15807                )));
15808            }
15809        };
15810        // v7.9.14 — accept extra comma-separated columns inside
15811        // the index key parens (`CREATE INDEX … (a, b, c)`).
15812        // mailrs F2. Each extra column may carry an optional
15813        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15814        // — parsed and discarded; SPG doesn't honour direction
15815        // on a BTree index today (column ordering is intrinsic
15816        // to the storage). v7.10 will widen to genuine composite
15817        // index keys.
15818        let mut extra_columns: Vec<String> = Vec::new();
15819        // The leading column may also have ASC/DESC after it — and that
15820        // one is the column SPG indexes, so its clause is kept.
15821        let key_order = self.consume_optional_index_column_qualifiers();
15822        while matches!(self.peek(), Token::Comma) {
15823            self.advance();
15824            let extra = self.expect_ident_like()?;
15825            let _ = self.consume_optional_index_column_qualifiers();
15826            extra_columns.push(extra);
15827        }
15828        if !matches!(self.peek(), Token::RParen) {
15829            return Err(self.err(format!(
15830                "expected ')' after indexed column / expression, got {:?}",
15831                self.peek()
15832            )));
15833        }
15834        self.advance();
15835        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15836        // index-only-scan annotation. Bare ident (not a reserved
15837        // keyword) so we test by case-insensitive string match.
15838        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15839        {
15840            self.advance();
15841            if !matches!(self.peek(), Token::LParen) {
15842                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15843            }
15844            self.advance();
15845            let mut cols = Vec::new();
15846            loop {
15847                cols.push(self.expect_ident_like()?);
15848                match self.peek() {
15849                    Token::Comma => {
15850                        self.advance();
15851                    }
15852                    Token::RParen => {
15853                        self.advance();
15854                        break;
15855                    }
15856                    other => {
15857                        return Err(self.err(format!(
15858                            "expected ',' or ')' in INCLUDE list, got {other:?}"
15859                        )));
15860                    }
15861                }
15862            }
15863            cols
15864        } else {
15865            Vec::new()
15866        };
15867        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15868        // storage parameters. pgvector emits `WITH (lists = N)` for
15869        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15870        // SPG's HNSW picks its own parameters today (tunable via
15871        // env vars), so the WITH clause is informational and dropped.
15872        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15873            self.advance();
15874            if !matches!(self.peek(), Token::LParen) {
15875                return Err(self.err(format!(
15876                    "expected '(' after WITH in CREATE INDEX, got {:?}",
15877                    self.peek()
15878                )));
15879            }
15880            self.advance();
15881            loop {
15882                if matches!(self.peek(), Token::RParen) {
15883                    self.advance();
15884                    break;
15885                }
15886                // Drain `key = value` or bare `key` tokens.
15887                let _ = self.advance(); // key
15888                if matches!(self.peek(), Token::Eq) {
15889                    self.advance();
15890                    let _ = self.advance(); // value (int / string / ident)
15891                }
15892                match self.peek() {
15893                    Token::Comma => {
15894                        self.advance();
15895                    }
15896                    Token::RParen => {
15897                        self.advance();
15898                        break;
15899                    }
15900                    other => {
15901                        return Err(self.err(format!(
15902                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
15903                        )));
15904                    }
15905                }
15906            }
15907        }
15908        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15909        // which sits between the key list and the WHERE clause.
15910        let mut nulls_not_distinct = false;
15911        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15912            let n1 = self.tokens.get(self.pos + 1);
15913            let n2 = self.tokens.get(self.pos + 2);
15914            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15915                self.advance(); // NULLS
15916                self.advance(); // NOT
15917                self.advance(); // DISTINCT
15918                nulls_not_distinct = true;
15919            } else if matches!(n1, Some(Token::Distinct)) {
15920                self.advance(); // NULLS
15921                self.advance(); // DISTINCT
15922            }
15923        }
15924        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15925        let partial_predicate = if matches!(self.peek(), Token::Where) {
15926            self.advance();
15927            Some(self.parse_expr(0)?)
15928        } else {
15929            None
15930        };
15931        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15932        // sense: uniqueness over an ANN structure has no clean
15933        // semantics. Reject early. (BRIN UNIQUE is similarly
15934        // meaningless — block both.)
15935        if is_unique && !matches!(method, IndexMethod::BTree) {
15936            return Err(self.err(alloc::format!(
15937                "UNIQUE is only supported on BTree indexes, got USING {:?}",
15938                method
15939            )));
15940        }
15941        Ok(Statement::CreateIndex(CreateIndexStatement {
15942            concurrently,
15943            name,
15944            key_order,
15945            key_collation,
15946            table,
15947            column,
15948            nulls_not_distinct,
15949            method,
15950            if_not_exists,
15951            included_columns,
15952            partial_predicate,
15953            extra_columns: extra_columns.clone(),
15954            expression,
15955            is_unique,
15956            opclass,
15957            method_name,
15958        }))
15959    }
15960
15961    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15962    /// column-level `REFERENCES ...` clause. The trailing FK is
15963    /// normalised into table-level shape (single-element columns +
15964    /// parent_columns) so the engine sees one uniform constraint list.
15965    fn parse_column_def_with_fk(
15966        &mut self,
15967    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15968        let col = self.parse_column_def()?;
15969        // v7.39 (round 308, V29) — an explicitly named inline FK:
15970        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15971        // loop leaves this spelling intact precisely so the name can be
15972        // kept here; PG reports it in violation messages and matches it
15973        // in `SET CONSTRAINTS`.
15974        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15975        {
15976            self.advance();
15977            Some(self.expect_ident_like()?)
15978        } else {
15979            None
15980        };
15981        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15982        let inline_references = matches!(
15983            self.peek(),
15984            Token::Ident(s) if s.eq_ignore_ascii_case("references")
15985        );
15986        if !inline_references {
15987            return Ok((col, None));
15988        }
15989        let (
15990            parent_table,
15991            parent_columns,
15992            on_delete,
15993            on_update,
15994            match_type,
15995            deferrable,
15996            initially_deferred,
15997        ) = self.parse_references_tail(1)?;
15998        let fk = ForeignKeyConstraint {
15999            name: declared_name,
16000            columns: vec![col.name.clone()],
16001            parent_table,
16002            parent_columns,
16003            on_delete,
16004            on_update,
16005            match_type,
16006            deferrable,
16007            initially_deferred,
16008        };
16009        Ok((col, Some(fk)))
16010    }
16011
16012    /// v7.13.0 — parse a column type (consuming the type ident and
16013    /// any trailing parameters / `[]`), without surrounding column
16014    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16015    /// Returns the resolved `ColumnTypeName` plus implied
16016    /// `(auto_increment, not_null)` flags from PG SERIAL family
16017    /// shorthands — callers that don't expect those (ALTER COLUMN
16018    /// TYPE) can discard them.
16019    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16020        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16021        Ok(ty)
16022    }
16023
16024    #[allow(clippy::type_complexity)]
16025    fn parse_type_with_implied_flags(
16026        &mut self,
16027    ) -> Result<
16028        (
16029            ColumnTypeName,
16030            bool,
16031            bool,
16032            Option<String>,
16033            Collation,
16034            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16035            bool,
16036            // v7.39 (round 676) — the collation NAME as written, which the
16037            // `Collation` enum above cannot carry.
16038            Option<String>,
16039            bool,
16040            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16041            // list captured at type-parse time. None for all
16042            // non-ENUM types.
16043            Option<Vec<String>>,
16044            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16045            // list. Distinct from ENUM (subset semantics).
16046            Option<Vec<String>>,
16047            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16048            // width, lost when the type collapses to SmallInt / Int.
16049            Option<MysqlIntWidth>,
16050            // v7.39 (round 424) — declared fractional-seconds precision of a
16051            // MySQL temporal column (bare spelling = 0). None under PG.
16052            Option<u8>,
16053        ),
16054        ParseError,
16055    > {
16056        let mut ty_ident = match self.advance() {
16057            Token::Ident(s) => s,
16058            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16059            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16060            // '<span>'` literal grammar. As a column type it lands
16061            // here directly; downstream resolution still uses the
16062            // canonical lowercase string.
16063            Token::Interval => "interval".to_string(),
16064            other => {
16065                return Err(ParseError {
16066                    message: format!("expected column type, got {other:?}"),
16067                    token_pos: self.consumed_pos(),
16068                });
16069            }
16070        };
16071        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16072        // pg_dump qualifies extension types (`public.vector(1024)`).
16073        // SPG is single-namespace; drop the schema and resolve the
16074        // bare type — same treatment table names already get.
16075        while matches!(self.peek(), Token::Dot) {
16076            self.advance();
16077            ty_ident = self.expect_ident_like()?;
16078        }
16079        let mut implied_auto_increment = false;
16080        let mut implied_not_null = false;
16081        let mut user_type_ref: Option<String> = None;
16082        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16083        // value list, captured here and bubbled up through the
16084        // ColumnDef so the engine can attach it to the column
16085        // schema (and validate INSERT cells against it).
16086        let mut inline_enum_variants: Option<Vec<String>> = None;
16087        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16088        let mut inline_set_variants: Option<Vec<String>> = None;
16089        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16090        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16091        // collapses to SmallInt / Int. Only under the MySQL dialect.
16092        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16093        // v7.39 (round 424) — the declared fractional-seconds precision of a
16094        // MySQL temporal column. Set by the temporal arms below; stays None
16095        // for PG (whose temporal columns keep full microseconds).
16096        let mut mysql_fsp: Option<u8> = None;
16097        let mut ty = match ty_ident.as_str() {
16098            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16099            "smallserial" | "serial2" => {
16100                implied_auto_increment = true;
16101                implied_not_null = true;
16102                ColumnTypeName::SmallInt
16103            }
16104            "serial" | "serial4" => {
16105                implied_auto_increment = true;
16106                implied_not_null = true;
16107                ColumnTypeName::Int
16108            }
16109            "bigserial" | "serial8" => {
16110                implied_auto_increment = true;
16111                implied_not_null = true;
16112                ColumnTypeName::BigInt
16113            }
16114            // MySQL flavours we accept by aliasing to the closest SPG
16115            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16116            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16117            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16118            // without semantic effect.
16119            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16120            // PG's internal type names; pg_dump and hand-written PG schemas
16121            // use them interchangeably with smallint / int / bigint (the cast
16122            // path already accepted them, only the column grammar didn't).
16123            "smallint" | "int2" => {
16124                // v7.14.0 — MySQL display-width on integers
16125                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16126                // parenthesised number is purely cosmetic — it
16127                // doesn't change storage. Accept + discard.
16128                self.consume_optional_paren_size();
16129                ColumnTypeName::SmallInt
16130            }
16131            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16132            // canonical encoding for BOOLEAN. Every MySQL driver
16133            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16134            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16135            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16136            // gave the customer i16-shaped values where the app
16137            // expected bool — a Tier-A silent type drift on
16138            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16139            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16140            // stay SmallInt (the legacy width-agnostic path).
16141            "tinyint" => {
16142                let width = self.peek_optional_paren_size_value();
16143                self.consume_optional_paren_size();
16144                if width == Some(1) {
16145                    ColumnTypeName::Bool
16146                } else {
16147                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16148                    // lost width so the write path can enforce -128..127.
16149                    if self.mysql_dialect {
16150                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16151                    }
16152                    ColumnTypeName::SmallInt
16153                }
16154            }
16155            "mediumint" => {
16156                self.consume_optional_paren_size();
16157                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16158                if self.mysql_dialect {
16159                    mysql_int_width = Some(MysqlIntWidth::Medium);
16160                }
16161                ColumnTypeName::Int
16162            }
16163            "int" | "integer" | "int4" => {
16164                self.consume_optional_paren_size();
16165                ColumnTypeName::Int
16166            }
16167            "bigint" | "int8" => {
16168                self.consume_optional_paren_size();
16169                ColumnTypeName::BigInt
16170            }
16171            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16172            // (mailrs round-5 G6). Consume the optional `PRECISION`
16173            // tail when the type keyword was `double` / `DOUBLE`.
16174            //
16175            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16176            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16177            // p in 1..=24 is real, 25..=53 is double precision, and
16178            // anything else is an error.
16179            "float" | "double" | "real" => {
16180                if ty_ident.eq_ignore_ascii_case("double")
16181                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16182                {
16183                    self.advance();
16184                }
16185                if ty_ident.eq_ignore_ascii_case("real") {
16186                    // v7.39 (round 274) — the two dialects genuinely
16187                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16188                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16189                    // 32-bit globally and thereby narrowed the stored
16190                    // precision of every MySQL REAL column.
16191                    if self.mysql_dialect {
16192                        ColumnTypeName::Float
16193                    } else {
16194                        ColumnTypeName::Real
16195                    }
16196                } else if ty_ident.eq_ignore_ascii_case("float")
16197                    && self.mysql_dialect
16198                    && matches!(self.peek(), Token::LParen)
16199                    && self.peek_paren_has_comma()
16200                {
16201                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16202                    // display form (`FLOAT(10,2)`), which PG has no
16203                    // equivalent of. It was `syntax error at or near ","`,
16204                    // so the whole CREATE failed. The digits are a display
16205                    // hint only; SPG stores the full double.
16206                    self.consume_optional_paren_size();
16207                    ColumnTypeName::Float
16208                } else if ty_ident.eq_ignore_ascii_case("float")
16209                    && matches!(self.peek(), Token::LParen)
16210                {
16211                    // PG words the two bounds differently, and
16212                    // parse_paren_size already rejects a zero.
16213                    let p = self.parse_paren_size("FLOAT")?;
16214                    if p > 53 {
16215                        return Err(self.err(String::from(
16216                            "precision for type float must be less than 54 bits",
16217                        )));
16218                    }
16219                    if p <= 24 {
16220                        ColumnTypeName::Real
16221                    } else {
16222                        ColumnTypeName::Float
16223                    }
16224                } else {
16225                    ColumnTypeName::Float
16226                }
16227            }
16228            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16229            "float4" => ColumnTypeName::Real,
16230            "float8" => ColumnTypeName::Float,
16231            "text" => ColumnTypeName::Text,
16232            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16233            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16234            // real MySQL schema and NONE of them existed: the CREATE
16235            // failed outright with `type "blob" does not exist`, so the
16236            // table was never made. The sizes differ only in MySQL's
16237            // maximum length, which SPG does not cap, so they collapse
16238            // onto TEXT and BYTEA the way the unsized spellings do.
16239            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16240            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16241            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16242            // enforce, consumed so the declaration parses.
16243            "varbinary" | "binary" => {
16244                self.consume_optional_paren_size();
16245                ColumnTypeName::Bytes
16246            }
16247            "name" => ColumnTypeName::Name,
16248            "xid" => ColumnTypeName::Xid,
16249            "oid" => ColumnTypeName::Oid,
16250            "xid8" => ColumnTypeName::Xid8,
16251            "bool" | "boolean" => ColumnTypeName::Bool,
16252            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16253            // an unbounded `character varying`, which the arm below has always
16254            // read as text. Only the short spelling demanded a length, so
16255            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16256            // there is — failed on `VARCHAR type requires (N)` while the long
16257            // spelling of the same thing was accepted. The same asymmetry
16258            // round 613 closed on the CAST side, here on the DDL side.
16259            "varchar" => {
16260                if matches!(self.peek(), Token::LParen) {
16261                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16262                } else {
16263                    ColumnTypeName::Text
16264                }
16265            }
16266            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16267            // `character` below (SQL standard).
16268            "char" => {
16269                if matches!(self.peek(), Token::LParen) {
16270                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16271                } else {
16272                    ColumnTypeName::Char(1)
16273                }
16274            }
16275            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16276            // `character(n)` = char, bare `character` = char(1). Unbounded
16277            // `character varying` maps to text.
16278            "character" => {
16279                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16280                    self.advance();
16281                    if matches!(self.peek(), Token::LParen) {
16282                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16283                    } else {
16284                        ColumnTypeName::Text
16285                    }
16286                } else if matches!(self.peek(), Token::LParen) {
16287                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16288                } else {
16289                    ColumnTypeName::Char(1)
16290                }
16291            }
16292            "vector" => {
16293                let dim = self.parse_paren_size("VECTOR")?;
16294                let encoding = self.parse_optional_vector_encoding()?;
16295                ColumnTypeName::Vector { dim, encoding }
16296            }
16297            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16298            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16299            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16300            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16301            // DECIMAL(10,2))` — how nearly every money column is written,
16302            // in either dialect — was a syntax error and the table was
16303            // never created. `FIXED` is MySQL's alias alone, so it is
16304            // taken only in that dialect.
16305            "numeric" | "decimal" | "dec" => {
16306                let (precision, scale) = self.parse_optional_numeric_params()?;
16307                ColumnTypeName::Numeric(precision, scale)
16308            }
16309            "fixed" if self.mysql_dialect => {
16310                let (precision, scale) = self.parse_optional_numeric_params()?;
16311                ColumnTypeName::Numeric(precision, scale)
16312            }
16313            "date" => ColumnTypeName::Date,
16314            // MySQL's `DATETIME` is the same domain as standard
16315            // `TIMESTAMP` — accept both spellings.
16316            "timestamp" | "datetime" => {
16317                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16318                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16319                // TIME ZONE` clause, so consume it first.
16320                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16321                // (it truncates on write and pads on render), so capture it;
16322                // a bare spelling means precision 0 there. PG stores µs always
16323                // and keeps `None`.
16324                let n = self.take_optional_paren_size();
16325                if self.mysql_dialect {
16326                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16327                }
16328                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16329                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16330                // the full form. SPG canonicalises:
16331                //   - WITH TIME ZONE    → Timestamptz
16332                //   - WITHOUT TIME ZONE → Timestamp
16333                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16334                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16335                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16336                {
16337                    self.advance(); // WITH
16338                    self.advance(); // TIME
16339                    self.advance(); // ZONE
16340                    ColumnTypeName::Timestamptz
16341                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16342                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16343                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16344                {
16345                    self.advance(); // WITHOUT
16346                    self.advance(); // TIME
16347                    self.advance(); // ZONE
16348                    ColumnTypeName::Timestamp
16349                } else {
16350                    // A second `(precision)` cannot legally follow, but the
16351                    // old grammar tolerated it; keep that tolerance.
16352                    self.consume_optional_paren_size();
16353                    ColumnTypeName::Timestamp
16354                }
16355            }
16356            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16357            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16358            // only PG-wire OID differs.
16359            "timestamptz" => {
16360                self.consume_optional_paren_size();
16361                ColumnTypeName::Timestamptz
16362            }
16363            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16364            // validation. We accept the JSONB spelling too because
16365            // most PG clients default to it; SPG doesn't distinguish
16366            // the two (no path-operator perf advantage to model).
16367            "json" => ColumnTypeName::Json,
16368            "jsonb" => ColumnTypeName::Jsonb,
16369            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16370            // surface here. Same storage shape; mapping happens at
16371            // the engine side via the ColumnTypeName → DataType
16372            // resolver. Literal forms are handled at coerce_value
16373            // time so the lexer stays untouched.
16374            "bytea" | "bytes" => ColumnTypeName::Bytes,
16375            // v7.17.0 Phase 7 — PG network address types
16376            // v7.17.0 had a Text-backed fallback here for
16377            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16378            // each to a first-class type; the keywords are
16379            // bound below in the ζ-A block.
16380            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16381            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16382            // arrives in v7.12.1+; the type itself loads here so
16383            // mailrs's `scripts/init-schema.sql` runs unmodified.
16384            "tsvector" => ColumnTypeName::TsVector,
16385            "tsquery" => ColumnTypeName::TsQuery,
16386            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16387            // surface for Django / Rails / Hibernate's default
16388            // PK pattern.
16389            "uuid" => ColumnTypeName::Uuid,
16390            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16391            // Storage = three-field {months, days, micros}, catalog
16392            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16393            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16394            "interval" => {
16395                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16396                // SECOND` and an optional `(p)` precision. SPG stores the full
16397                // {months,days,micros}; consume + ignore the qualifier/precision.
16398                while matches!(self.peek(), Token::To)
16399                    || matches!(self.peek(), Token::Ident(s) if matches!(
16400                        s.to_ascii_lowercase().as_str(),
16401                        "year" | "month" | "day" | "hour" | "minute" | "second"
16402                    ))
16403                {
16404                    self.advance();
16405                }
16406                self.consume_optional_paren_size();
16407                ColumnTypeName::Interval
16408            }
16409            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16410            // i64 microseconds since 00:00:00. Wire OID 1083.
16411            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16412            "time" => {
16413                // v7.39 (round 424) — MySQL TIME carries a semantic
16414                // fractional-seconds precision, bare meaning 0.
16415                let n = self.take_optional_paren_size();
16416                if self.mysql_dialect {
16417                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16418                }
16419                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16420                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16421                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16422                {
16423                    self.advance();
16424                    self.advance();
16425                    self.advance();
16426                    ColumnTypeName::TimeTz
16427                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16428                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16429                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16430                {
16431                    self.advance();
16432                    self.advance();
16433                    self.advance();
16434                    ColumnTypeName::Time
16435                } else {
16436                    ColumnTypeName::Time
16437                }
16438            }
16439            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16440            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16441            "year" => ColumnTypeName::Year,
16442            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16443            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16444            "timetz" => ColumnTypeName::TimeTz,
16445            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16446            // Wire OID 790.
16447            "money" => ColumnTypeName::Money,
16448            // v7.17.0 Phase 3.P0-38 — PG range types.
16449            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16450            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16451            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16452            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16453            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16454            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16455            // v7.37.5 δ — PG 14+ multirange keywords.
16456            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16457            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16458            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16459            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16460            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16461            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16462            // v7.37.5 ε — PG geometry scalar keywords.
16463            "point" => ColumnTypeName::Point,
16464            "lseg" => ColumnTypeName::Lseg,
16465            "path" => ColumnTypeName::Path,
16466            "box" => ColumnTypeName::PgBox,
16467            "polygon" => ColumnTypeName::Polygon,
16468            "line" => ColumnTypeName::Line,
16469            "circle" => ColumnTypeName::Circle,
16470            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16471            "inet" => ColumnTypeName::Inet,
16472            "cidr" => ColumnTypeName::Cidr,
16473            "macaddr" => ColumnTypeName::Macaddr,
16474            "macaddr8" => ColumnTypeName::Macaddr8,
16475            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16476            // width in the value, so the optional `(N)` typmod is accepted and
16477            // ignored (the column stores whatever width it's given).
16478            "bit" => {
16479                let varying = matches!(
16480                    self.peek(),
16481                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16482                );
16483                if varying {
16484                    self.advance();
16485                }
16486                // v7.39 (round 281) — the length used to be parsed and
16487                // dropped, so `bit(3)` accepted a five-bit string.
16488                let n = if matches!(self.peek(), Token::LParen) {
16489                    self.parse_paren_size("BIT")?
16490                } else {
16491                    0
16492                };
16493                if varying {
16494                    ColumnTypeName::BitVarying(n)
16495                } else {
16496                    ColumnTypeName::Bit(n)
16497                }
16498            }
16499            "varbit" => {
16500                let n = if matches!(self.peek(), Token::LParen) {
16501                    self.parse_paren_size("VARBIT")?
16502                } else {
16503                    0
16504                };
16505                ColumnTypeName::BitVarying(n)
16506            }
16507            "xml" => ColumnTypeName::Xml,
16508            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16509            "hstore" => ColumnTypeName::Hstore,
16510            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16511            // `ENUM('a','b','c')`. Storage is TEXT; the value
16512            // list lands on `inline_enum_variants` for the
16513            // engine to validate INSERT cells against. Empty
16514            // value list is a parse error (matches MySQL).
16515            "enum" => {
16516                // Expect the opening `(`.
16517                if !matches!(self.peek(), Token::LParen) {
16518                    return Err(self.err(alloc::format!(
16519                        "expected '(' after ENUM, got {:?}",
16520                        self.peek()
16521                    )));
16522                }
16523                self.advance();
16524                let mut variants: Vec<String> = Vec::new();
16525                loop {
16526                    match self.advance() {
16527                        Token::String(s) => variants.push(s),
16528                        other => {
16529                            return Err(self.err(alloc::format!(
16530                                "ENUM(...) expects string literal variants, got {other:?}"
16531                            )));
16532                        }
16533                    }
16534                    match self.peek() {
16535                        Token::Comma => {
16536                            self.advance();
16537                            continue;
16538                        }
16539                        Token::RParen => {
16540                            self.advance();
16541                            break;
16542                        }
16543                        other => {
16544                            return Err(self.err(alloc::format!(
16545                                "expected ',' or ')' in ENUM(...), got {other:?}"
16546                            )));
16547                        }
16548                    }
16549                }
16550                if variants.is_empty() {
16551                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16552                }
16553                inline_enum_variants = Some(variants);
16554                // Storage is plain TEXT; the variant list lives on
16555                // the ColumnSchema side.
16556                ColumnTypeName::Text
16557            }
16558            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16559            // `SET('a','b','c')`. Same parse shape as ENUM;
16560            // semantics differ (subset rather than pick-one).
16561            "set" => {
16562                if !matches!(self.peek(), Token::LParen) {
16563                    return Err(self.err(alloc::format!(
16564                        "expected '(' after SET, got {:?}",
16565                        self.peek()
16566                    )));
16567                }
16568                self.advance();
16569                let mut variants: Vec<String> = Vec::new();
16570                loop {
16571                    match self.advance() {
16572                        Token::String(s) => variants.push(s),
16573                        other => {
16574                            return Err(self.err(alloc::format!(
16575                                "SET(...) expects string literal variants, got {other:?}"
16576                            )));
16577                        }
16578                    }
16579                    match self.peek() {
16580                        Token::Comma => {
16581                            self.advance();
16582                            continue;
16583                        }
16584                        Token::RParen => {
16585                            self.advance();
16586                            break;
16587                        }
16588                        other => {
16589                            return Err(self.err(alloc::format!(
16590                                "expected ',' or ')' in SET(...), got {other:?}"
16591                            )));
16592                        }
16593                    }
16594                }
16595                if variants.is_empty() {
16596                    return Err(self.err("SET(...) must declare at least one variant".into()));
16597                }
16598                inline_set_variants = Some(variants);
16599                ColumnTypeName::Text
16600            }
16601            _other => {
16602                // v7.17.0 Phase 1.4 — unknown ident → defer
16603                // resolution to the engine. Stored as Text in
16604                // ColumnTypeName + the original name carried as
16605                // `user_type_ref` so CREATE TABLE can look up
16606                // user-defined enum / domain types.
16607                user_type_ref = Some(ty_ident.clone());
16608                ColumnTypeName::Text
16609            }
16610        };
16611        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16612        // right after the type keyword. Pre-4.4 SPG consumed +
16613        // discarded the keyword, leaving a customer column
16614        // declared `id INT UNSIGNED NOT NULL` silently accepting
16615        // negative values — a Tier-A correctness drift where
16616        // application invariants (auto-increment-IDs never
16617        // negative) silently broke on cutover. Now: capture as
16618        // a column flag, persist on the schema, enforce at
16619        // INSERT / UPDATE time.
16620        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16621        {
16622            self.advance();
16623            true
16624        } else {
16625            false
16626        };
16627        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16628        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16629        // stores text as UTF-8 always so CHARACTER SET is still a
16630        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16631        // name: it gets classified into a `Collation` variant the
16632        // engine consults at WHERE-eval time. PG `default` /
16633        // `pg_catalog.default` / `C` / `POSIX` collations all
16634        // resolve to `Binary` (the prior behaviour); `_ci` /
16635        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16636        // The schema-qualifier form (`pg_catalog.default`) lexes
16637        // as `Ident '.' Ident` — peek for the `.` and consume both
16638        // halves so it's treated as one collation name. PG's
16639        // `IDENT.IDENT` collation form (which can appear here) is
16640        // resolved by Collation::from_collation_name on the bare
16641        // identifier after the dot.
16642        let mut collation = Collation::Binary;
16643        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16644        // clause was written. The engine needs this to tell an explicit
16645        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16646        // clause at all: both resolve to `Collation::Binary`, but under the
16647        // MySQL dialect the latter takes the folding default collation.
16648        let mut collation_explicit = false;
16649        let mut collation_name: Option<alloc::string::String> = None;
16650        loop {
16651            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16652                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16653            {
16654                self.advance(); // CHARACTER
16655                self.advance(); // SET
16656                if matches!(
16657                    self.peek(),
16658                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16659                ) {
16660                    self.advance();
16661                }
16662                continue;
16663            }
16664            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16665                self.advance(); // COLLATE
16666                // Accept Ident / QuotedIdent / String AND the
16667                // keyword-tokenised `Default` (PG `pg_catalog.default`
16668                // and bare `DEFAULT` collation names — `default` is a
16669                // reserved word so the lexer hands back Token::Default
16670                // not Token::Ident).
16671                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16672                    match this.peek().clone() {
16673                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16674                            this.advance();
16675                            Some(s)
16676                        }
16677                        Token::Default => {
16678                            this.advance();
16679                            Some(alloc::string::String::from("default"))
16680                        }
16681                        _ => None,
16682                    }
16683                };
16684                let raw = if let Some(head) = read_collation_atom(self) {
16685                    // Schema-qualified PG form: `pg_catalog.default`.
16686                    if matches!(self.peek(), Token::Dot) {
16687                        self.advance();
16688                        let tail = read_collation_atom(self).unwrap_or_default();
16689                        alloc::format!("{head}.{tail}")
16690                    } else {
16691                        head
16692                    }
16693                } else {
16694                    alloc::string::String::new()
16695                };
16696                if !raw.is_empty() {
16697                    collation_explicit = true;
16698                    // v7.39 (round 676) — keep the name too. The enum below
16699                    // folds C / POSIX / en_US / default into one value, and
16700                    // `pg_attribute.attcollation` has to tell them apart.
16701                    // The schema qualifier goes: PG's `pg_catalog.default`
16702                    // and a bare `default` name the same collation.
16703                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16704                    // encoding suffix. Round 676 used `rsplit('.')` for
16705                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16706                    // PG writes `pg_catalog.default` (qualifier) and
16707                    // `en_US.utf8` (locale + encoding) with the same
16708                    // separator. Only `pg_catalog.` is a qualifier, and it
16709                    // is the only one PG's own dumps emit.
16710                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16711                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16712                    collation_name = Some(alloc::string::String::from(bare));
16713                    let parsed = Collation::from_collation_name(&raw);
16714                    // Last COLLATE clause wins, but `Binary` from a
16715                    // bare keyword like `default` should not
16716                    // silently downgrade a stronger one set earlier
16717                    // on the same column. v7.17 only ships one
16718                    // non-Binary variant so a simple OR is enough.
16719                    if parsed != Collation::Binary {
16720                        collation = parsed;
16721                    }
16722                }
16723                continue;
16724            }
16725            break;
16726        }
16727        // v7.10.10 — postfix `[]` widens the base type to its array
16728        // type. PG accepts `TYPE[]` after any base type and so does
16729        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16730        // all through; the old "only TEXT[]" note was stale).
16731        if matches!(self.peek(), Token::LBracket) {
16732            self.advance();
16733            if !matches!(self.peek(), Token::RBracket) {
16734                return Err(self.err(alloc::format!(
16735                    "TEXT[] takes no dimension; got {:?}",
16736                    self.peek()
16737                )));
16738            }
16739            self.advance();
16740            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16741            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16742            // still error here.
16743            ty = match ty {
16744                ColumnTypeName::Text => ColumnTypeName::TextArray,
16745                ColumnTypeName::Int => ColumnTypeName::IntArray,
16746                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16747                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16748                // `[]` grammar. Wire OID 1187.
16749                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16750                // v7.37.5 γ — full PG array-of-scalar family.
16751                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16752                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16753                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16754                // NUMERIC(p, s) loses its precision params at the
16755                // array level (matches PG: `NUMERIC[]` is untyped,
16756                // per-element precision flows through values).
16757                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16758                ColumnTypeName::Date => ColumnTypeName::DateArray,
16759                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16760                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16761                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16762                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16763                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16764                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16765                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16766                // the array level (matches PG semantics where the
16767                // element precision is per-row, not column-wide).
16768                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16769                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16770                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16771                // follow-up.
16772                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16773                other => {
16774                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16775                }
16776            };
16777            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16778            // for INT/TEXT/BIGINT. Anything else is an error.
16779            if matches!(self.peek(), Token::LBracket) {
16780                self.advance();
16781                if !matches!(self.peek(), Token::RBracket) {
16782                    return Err(self.err(alloc::format!(
16783                        "TYPE[][] second dimension takes no size; got {:?}",
16784                        self.peek()
16785                    )));
16786                }
16787                self.advance();
16788                ty = match ty {
16789                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16790                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16791                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16792                    // v7.39 (read01 round 75) — bool[][].
16793                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16794                    other => {
16795                        return Err(self.err(alloc::format!(
16796                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16797                             TEXT[][] only; got {other:?}"
16798                        )));
16799                    }
16800                };
16801            }
16802        }
16803        Ok((
16804            ty,
16805            implied_auto_increment,
16806            implied_not_null,
16807            user_type_ref,
16808            collation,
16809            collation_explicit,
16810            collation_name,
16811            is_unsigned,
16812            inline_enum_variants,
16813            inline_set_variants,
16814            mysql_int_width,
16815            mysql_fsp,
16816        ))
16817    }
16818
16819    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16820        // v7.20 — PG reserves the table-constraint keywords, so a
16821        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16822        // malformed constraint clause (e.g. `UNIQUE a` missing its
16823        // parens), not a column named "unique". Since v7.17's
16824        // unknown-type leniency (`user_type_ref`) such a clause
16825        // would otherwise parse as a column with a user-defined
16826        // type — silently accepting invalid DDL. Quoted
16827        // identifiers ("unique" / `unique`) remain valid names.
16828        if let Token::Ident(s) = self.peek()
16829            && [
16830                "unique",
16831                "primary",
16832                "foreign",
16833                "constraint",
16834                "check",
16835                "references",
16836                "exclude",
16837            ]
16838            .iter()
16839            .any(|kw| s.eq_ignore_ascii_case(kw))
16840        {
16841            return Err(self.err(alloc::format!(
16842                "unexpected reserved keyword '{s}' at start of column definition \
16843                 (malformed table constraint?)"
16844            )));
16845        }
16846        let name = self.expect_ident_like()?;
16847        let (
16848            ty,
16849            implied_auto_increment,
16850            implied_not_null,
16851            user_type_ref,
16852            collation,
16853            collation_explicit,
16854            collation_name,
16855            is_unsigned,
16856            inline_enum_variants,
16857            inline_set_variants,
16858            mysql_int_width,
16859            mysql_fsp,
16860        ) = self.parse_type_with_implied_flags()?;
16861        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16862        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16863        // each at most once.
16864        let mut default: Option<Expr> = None;
16865        let mut nullable = !implied_not_null;
16866        let mut nullability_seen = implied_not_null;
16867        let mut auto_increment = implied_auto_increment;
16868        let mut is_primary_key = false;
16869        let mut is_unique = false;
16870        let mut unique_nulls_not_distinct = false;
16871        let mut constraint_deferrable = false;
16872        let mut constraint_initially_deferred = false;
16873        let mut check: Option<Expr> = None;
16874        let mut on_update_runtime: Option<Expr> = None;
16875        let mut generated_stored_expr: Option<Box<Expr>> = None;
16876        let mut identity_always = false;
16877        loop {
16878            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16879            // not-null constraints by name and pg_dump emits them
16880            // inline: `id bigint CONSTRAINT contacts_id_not_null1
16881            // NOT NULL`. Accept and discard the name; whatever
16882            // constraint follows is parsed by the arms below.
16883            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16884                // v7.39 (round 308, V29) — a name on an inline
16885                // REFERENCES belongs to the FOREIGN KEY, and the caller
16886                // (`parse_column_def_with_fk`) is what builds it, so
16887                // leave the whole clause for it. Dropping the name here
16888                // is what made `CONSTRAINT fk_a REFERENCES …` come back
16889                // as the synthesised `c_pid_fkey` — which then could
16890                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16891                // `advance()` takes tokens by `mem::replace`, so there
16892                // is no rewinding once consumed.
16893                if matches!(
16894                    self.tokens.get(self.pos + 2),
16895                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16896                ) {
16897                    break;
16898                }
16899                self.advance();
16900                let _name = self.expect_ident_like()?;
16901                continue;
16902            }
16903            // v7.39 (round 379) — MySQL's SHORT generated-column form
16904            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16905            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16906            // below), but hand-written schemas and app migrations use this.
16907            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16908            // SPG computes-and-stores either way, like the long form.
16909            if matches!(self.peek(), Token::As) {
16910                self.advance();
16911                if !matches!(self.peek(), Token::LParen) {
16912                    return Err(self.err(alloc::format!(
16913                        "expected '(' after AS in a generated column, got {:?}",
16914                        self.peek()
16915                    )));
16916                }
16917                self.advance();
16918                let expr = self.parse_expr(0)?;
16919                if !matches!(self.peek(), Token::RParen) {
16920                    return Err(self.err(alloc::format!(
16921                        "expected ')' after AS (<expr>), got {:?}",
16922                        self.peek()
16923                    )));
16924                }
16925                self.advance();
16926                if matches!(self.peek(), Token::Ident(s)
16927                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16928                {
16929                    self.advance();
16930                }
16931                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16932                continue;
16933            }
16934            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16935            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16936            // the modern replacement for SERIAL in hand-written
16937            // schemas). Both flavours map onto the auto-increment
16938            // machinery — SPG's serial semantics ≈ BY DEFAULT;
16939            // ALWAYS's reject-explicit-values nuance is documented
16940            // leniency. Generated EXPRESSION columns
16941            // (`AS (expr) STORED`) are not supported: error loudly
16942            // instead of silently storing NULLs.
16943            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16944                self.advance();
16945                let mut saw_generated_always = false;
16946                match self.peek().clone() {
16947                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16948                        self.advance();
16949                        saw_generated_always = true;
16950                    }
16951                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16952                        self.advance();
16953                        if !matches!(self.peek(), Token::Default) {
16954                            return Err(self.err(alloc::format!(
16955                                "expected DEFAULT after GENERATED BY, got {:?}",
16956                                self.peek()
16957                            )));
16958                        }
16959                        self.advance();
16960                    }
16961                    other => {
16962                        return Err(self.err(alloc::format!(
16963                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16964                        )));
16965                    }
16966                }
16967                if !matches!(self.peek(), Token::As) {
16968                    return Err(self.err(alloc::format!(
16969                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16970                        self.peek()
16971                    )));
16972                }
16973                self.advance();
16974                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16975                // ( <expr> ) STORED` stored computed-column. The
16976                // expression is captured for the engine to recompute
16977                // on every INSERT / UPDATE. v7.37.7 accepts the
16978                // STORED keyword only; PG also has VIRTUAL, which
16979                // v7.37.7 carves out (sentori only uses STORED).
16980                if matches!(self.peek(), Token::LParen) {
16981                    self.advance();
16982                    let expr = self.parse_expr(0)?;
16983                    if !matches!(self.peek(), Token::RParen) {
16984                        return Err(self.err(alloc::format!(
16985                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16986                            self.peek()
16987                        )));
16988                    }
16989                    self.advance();
16990                    let stored = match self.peek() {
16991                        Token::Ident(s) | Token::QuotedIdent(s)
16992                            if s.eq_ignore_ascii_case("stored") =>
16993                        {
16994                            self.advance();
16995                            true
16996                        }
16997                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
16998                        // generated columns. SPG computes them on write and
16999                        // persists like STORED; the two are observably
17000                        // identical for query results (the value, recompute
17001                        // on base-column change, and NOT NULL enforcement all
17002                        // match), so a PG 18 schema/dump using VIRTUAL loads
17003                        // and behaves correctly. The compute-on-read storage
17004                        // saving is an invisible internal difference.
17005                        Token::Ident(s) | Token::QuotedIdent(s)
17006                            if s.eq_ignore_ascii_case("virtual") =>
17007                        {
17008                            self.advance();
17009                            false
17010                        }
17011                        other => {
17012                            return Err(self.err(alloc::format!(
17013                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17014                                 got {other:?}"
17015                            )));
17016                        }
17017                    };
17018                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17019                    generated_stored_expr = Some(Box::new(expr));
17020                    continue;
17021                }
17022                self.expect_keyword_ident("identity")?;
17023                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17024                // consume the balanced parens and discard (SPG's
17025                // auto-increment is max+1-scan based).
17026                if matches!(self.peek(), Token::LParen) {
17027                    let mut depth = 0usize;
17028                    loop {
17029                        match self.advance() {
17030                            Token::LParen => depth += 1,
17031                            Token::RParen => {
17032                                depth -= 1;
17033                                if depth == 0 {
17034                                    break;
17035                                }
17036                            }
17037                            Token::Eof => {
17038                                return Err(self.err(
17039                                    "unterminated sequence-options parens after IDENTITY".into(),
17040                                ));
17041                            }
17042                            _ => {}
17043                        }
17044                    }
17045                }
17046                auto_increment = true;
17047                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17048                // can reject explicit non-DEFAULT INSERT values (unless
17049                // OVERRIDING SYSTEM VALUE) the way PG does.
17050                identity_always = saw_generated_always;
17051                // PG identity columns are implicitly NOT NULL.
17052                nullable = false;
17053                continue;
17054            }
17055            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17056            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17057            // is accepted today. The "ON" token is an Ident
17058            // (not reserved) — peek before consuming.
17059            if matches!(self.peek(), Token::On)
17060                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17061            {
17062                self.advance(); // ON
17063                self.advance(); // update
17064                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17065                let next = self.peek().clone();
17066                match next {
17067                    Token::Ident(s) | Token::QuotedIdent(s)
17068                        if s.eq_ignore_ascii_case("current_timestamp") =>
17069                    {
17070                        self.advance();
17071                        // Optional `(N)` precision.
17072                        if matches!(self.peek(), Token::LParen) {
17073                            self.advance();
17074                            if !matches!(self.peek(), Token::Integer(_)) {
17075                                return Err(self.err(alloc::format!(
17076                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17077                                    self.peek()
17078                                )));
17079                            }
17080                            self.advance();
17081                            if !matches!(self.peek(), Token::RParen) {
17082                                return Err(self.err(alloc::format!(
17083                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17084                                    self.peek()
17085                                )));
17086                            }
17087                            self.advance();
17088                        }
17089                        on_update_runtime = Some(Expr::FunctionCall {
17090                            name: "now".into(),
17091                            args: Vec::new(),
17092                        });
17093                        continue;
17094                    }
17095                    other => {
17096                        return Err(self.err(alloc::format!(
17097                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17098                        )));
17099                    }
17100                }
17101            }
17102            if matches!(self.peek(), Token::Default) {
17103                if default.is_some() {
17104                    return Err(self.err("DEFAULT specified twice".into()));
17105                }
17106                self.advance();
17107                default = Some(self.parse_expr(0)?);
17108                continue;
17109            }
17110            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17111            // token with NOT NULL and sits EARLIER in the loop than the
17112            // deferrability arm, so without the lookahead it was reported as
17113            // "NOT NULL specified twice" (or "expected NULL after NOT").
17114            if matches!(self.peek(), Token::Not)
17115                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17116            {
17117                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17118                self.consume_optional_deferrable_clauses()?;
17119                continue;
17120            }
17121            if matches!(self.peek(), Token::Not) {
17122                if nullability_seen {
17123                    return Err(self.err("NOT NULL specified twice".into()));
17124                }
17125                self.advance();
17126                if !matches!(self.peek(), Token::Null) {
17127                    return Err(self.err(format!(
17128                        "expected NULL after NOT in column def, got {:?}",
17129                        self.peek()
17130                    )));
17131                }
17132                self.advance();
17133                nullable = false;
17134                nullability_seen = true;
17135                continue;
17136            }
17137            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17138            // "this column is nullable" marker (the default in
17139            // standard SQL anyway). mysqldump emits it routinely
17140            // (`col TYPE NULL DEFAULT NULL` for nullable
17141            // timestamps etc). Accept + no-op.
17142            if matches!(self.peek(), Token::Null) {
17143                if nullability_seen && !nullable {
17144                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17145                    // sentence, PG18-measured (the table name is the
17146                    // caller's; the column half is exact).
17147                    return Err(self.err(alloc::format!(
17148                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17149                    )));
17150                }
17151                self.advance();
17152                nullable = true;
17153                nullability_seen = true;
17154                continue;
17155            }
17156            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17157            // arrives as a bare Ident. Match either, case-insensitive.
17158            if let Token::Ident(s) = self.peek()
17159                && (s.eq_ignore_ascii_case("auto_increment")
17160                    || s.eq_ignore_ascii_case("autoincrement"))
17161            {
17162                if auto_increment {
17163                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17164                }
17165                self.advance();
17166                auto_increment = true;
17167                continue;
17168            }
17169            // v7.9.13 — inline `PRIMARY KEY` column constraint
17170            // (mailrs F1). Implies `NOT NULL`. The engine creates
17171            // a BTree index for the PK column at CREATE TABLE time
17172            // so FK parent-side index lookups resolve.
17173            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17174            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17175            // spelling was a parse error, so a pg_dump carrying one stopped
17176            // mid-restore. The clauses are consumed by the same helper the FK
17177            // path has used since round 288 and recorded nowhere: SPG enforces
17178            // the constraint IMMEDIATELY either way, which fails earlier than
17179            // PG inside a transaction that violates-then-repairs — a refusal,
17180            // not a wrong answer. True deferral is the open remainder of F08.
17181            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17182                || (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                // v7.39 (round 711) — CARRIED now (the storing half of
17186                // F08); round 621 only consumed.
17187                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17188                constraint_deferrable |= d;
17189                constraint_initially_deferred |= idef;
17190                continue;
17191            }
17192            if let Token::Ident(s) = self.peek()
17193                && s.eq_ignore_ascii_case("primary")
17194            {
17195                if is_primary_key {
17196                    return Err(self.err("PRIMARY KEY specified twice".into()));
17197                }
17198                // Peek-ahead for the required `KEY` token.
17199                let next = self.tokens.get(self.pos + 1);
17200                let next_is_key = matches!(
17201                    next,
17202                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17203                );
17204                if !next_is_key {
17205                    return Err(self.err(format!(
17206                        "expected KEY after PRIMARY in column def, got {:?}",
17207                        next
17208                    )));
17209                }
17210                self.advance(); // PRIMARY
17211                self.advance(); // KEY
17212                is_primary_key = true;
17213                if nullability_seen && nullable {
17214                    return Err(self.err(
17215                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17216                    ));
17217                }
17218                nullable = false;
17219                nullability_seen = true;
17220                continue;
17221            }
17222            // v7.13.0 — inline `UNIQUE` column constraint
17223            // (mailrs round-5 G2). Fold into a single-column
17224            // table-level UNIQUE at CREATE TABLE post-process time.
17225            if let Token::Ident(s) = self.peek()
17226                && s.eq_ignore_ascii_case("unique")
17227            {
17228                if is_unique {
17229                    return Err(self.err("UNIQUE specified twice".into()));
17230                }
17231                self.advance();
17232                is_unique = true;
17233                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17234                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17235                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17236                    let n1 = self.tokens.get(self.pos + 1);
17237                    let n2 = self.tokens.get(self.pos + 2);
17238                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17239                        self.advance(); // NULLS
17240                        self.advance(); // NOT
17241                        self.advance(); // DISTINCT
17242                        unique_nulls_not_distinct = true;
17243                    } else if matches!(n1, Some(Token::Distinct)) {
17244                        self.advance(); // NULLS
17245                        self.advance(); // DISTINCT
17246                    }
17247                }
17248                continue;
17249            }
17250            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17251            // (mailrs round-5 G3). PG semantics: column-level
17252            // CHECK is equivalent to a table-level CHECK. Multiple
17253            // inline CHECKs on the same column AND together.
17254            if let Token::Ident(s) = self.peek()
17255                && s.eq_ignore_ascii_case("check")
17256            {
17257                self.advance();
17258                if !matches!(self.peek(), Token::LParen) {
17259                    return Err(self.err(alloc::format!(
17260                        "expected '(' after CHECK in column def, got {:?}",
17261                        self.peek()
17262                    )));
17263                }
17264                self.advance();
17265                let pred = self.parse_expr(0)?;
17266                if !matches!(self.peek(), Token::RParen) {
17267                    return Err(self.err(alloc::format!(
17268                        "expected ')' to close CHECK predicate, got {:?}",
17269                        self.peek()
17270                    )));
17271                }
17272                self.advance();
17273                check = Some(match check.take() {
17274                    Some(prev) => Expr::Binary {
17275                        op: BinOp::And,
17276                        lhs: Box::new(prev),
17277                        rhs: Box::new(pred),
17278                    },
17279                    None => pred,
17280                });
17281                continue;
17282            }
17283            break;
17284        }
17285        Ok(ColumnDef {
17286            name,
17287            ty,
17288            nullable,
17289            default,
17290            auto_increment,
17291            is_primary_key,
17292            is_unique,
17293            unique_nulls_not_distinct,
17294            constraint_deferrable,
17295            constraint_initially_deferred,
17296            check,
17297            user_type_ref,
17298            on_update_runtime,
17299            collation,
17300            collation_explicit,
17301            collation_name,
17302            is_unsigned,
17303            inline_enum_variants,
17304            inline_set_variants,
17305            generated_stored_expr,
17306            identity_always,
17307            mysql_int_width,
17308            mysql_fsp,
17309        })
17310    }
17311
17312    /// `NUMERIC` may appear without parameters, with one (precision
17313    /// only, scale=0), or with both. Returns `(precision, scale)` with
17314    /// 0 = unspecified for the bare form.
17315    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17316        if !matches!(self.peek(), Token::LParen) {
17317            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17318            // we surface it as precision=0 to mean "unconstrained" so
17319            // the engine doesn't need a separate variant.
17320            return Ok((0, 0));
17321        }
17322        self.advance();
17323        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17324        // it words the out-of-range case with the value it saw. SPG
17325        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17326        // accepts failed to parse at all; values wider than i128 are
17327        // carried by the arbitrary-precision form.
17328        let precision = match self.advance() {
17329            Token::Integer(n) if (1..=1000).contains(&n) => {
17330                u16::try_from(n).expect("range-checked")
17331            }
17332            Token::Integer(n) => {
17333                return Err(ParseError {
17334                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17335                    token_pos: self.consumed_pos(),
17336                });
17337            }
17338            other => {
17339                return Err(ParseError {
17340                    message: format!(
17341                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17342                    ),
17343                    token_pos: self.consumed_pos(),
17344                });
17345            }
17346        };
17347        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17348        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17349        // then overflows). A negative scale rounds to tens / hundreds / …
17350        let scale = if matches!(self.peek(), Token::Comma) {
17351            self.advance();
17352            let neg = if matches!(self.peek(), Token::Minus) {
17353                self.advance();
17354                true
17355            } else {
17356                false
17357            };
17358            match self.advance() {
17359                Token::Integer(n) => {
17360                    let signed = if neg { -n } else { n };
17361                    if !(-1000..=1000).contains(&signed) {
17362                        return Err(ParseError {
17363                            message: format!(
17364                                "NUMERIC scale {signed} must be between -1000 and 1000"
17365                            ),
17366                            token_pos: self.consumed_pos(),
17367                        });
17368                    }
17369                    i16::try_from(signed).expect("range-checked")
17370                }
17371                other => {
17372                    return Err(ParseError {
17373                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17374                        token_pos: self.consumed_pos(),
17375                    });
17376                }
17377            }
17378        } else {
17379            0
17380        };
17381        if !matches!(self.peek(), Token::RParen) {
17382            return Err(self.err(format!(
17383                "expected ')' to close NUMERIC params, got {:?}",
17384                self.peek()
17385            )));
17386        }
17387        self.advance();
17388        Ok((precision, scale))
17389    }
17390
17391    /// Parse `(N)` where `N` is a positive integer literal — used by the
17392    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17393    /// for the error message.
17394    /// v6.0.1: parse the optional `USING <encoding>` clause that
17395    /// follows `VECTOR(N)` in a column definition. Missing clause
17396    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17397    /// ident → `ParseError` listing the encodings recognised today.
17398    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17399        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17400            return Ok(VecEncoding::F32);
17401        }
17402        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17403        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17404        // consume the token when the very next token is a known
17405        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17406        // `USING` for the caller — it's the rewrite-expression form.
17407        let n1 = self.tokens.get(self.pos + 1);
17408        let next_is_encoding = matches!(
17409            n1,
17410            Some(Token::Ident(s))
17411                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17412        );
17413        if !next_is_encoding {
17414            return Ok(VecEncoding::F32);
17415        }
17416        self.advance();
17417        let enc_ident = match self.advance() {
17418            Token::Ident(s) => s,
17419            other => {
17420                return Err(self.err(format!(
17421                    "expected vector encoding after USING, got {other:?}"
17422                )));
17423            }
17424        };
17425        match enc_ident.to_ascii_lowercase().as_str() {
17426            "sq8" => Ok(VecEncoding::Sq8),
17427            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17428            // binary16 per-element storage.
17429            "half" => Ok(VecEncoding::F16),
17430            other => Err(self.err(format!(
17431                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17432            ))),
17433        }
17434    }
17435
17436    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17437    /// without consuming it. Returns `Some(N)` when the next
17438    /// tokens are `( <int> )`; None otherwise. Used by the
17439    /// TINYINT classifier to decide whether to map to Bool or
17440    /// SmallInt.
17441    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17442        if !matches!(self.peek(), Token::LParen) {
17443            return None;
17444        }
17445        let next = self.tokens.get(self.pos + 1)?;
17446        let n = match next {
17447            Token::Integer(n) => *n,
17448            _ => return None,
17449        };
17450        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17451            return None;
17452        }
17453        Some(n)
17454    }
17455
17456    /// v7.14.0 — consume an optional MySQL display-width
17457    /// parenthesised number after an integer type, returning
17458    /// nothing. `TINYINT(1)` etc.
17459    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17460    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17461    fn peek_paren_has_comma(&self) -> bool {
17462        let mut i = self.pos + 1;
17463        let mut depth = 1usize;
17464        while depth > 0 {
17465            match self.tokens.get(i) {
17466                Some(Token::LParen) => depth += 1,
17467                Some(Token::RParen) => depth -= 1,
17468                Some(Token::Comma) if depth == 1 => return true,
17469                None | Some(Token::Eof) => return false,
17470                _ => {}
17471            }
17472            i += 1;
17473        }
17474        false
17475    }
17476
17477    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17478    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17479    /// fractional-seconds precision that drives write truncation and render
17480    /// padding, where `consume_optional_paren_size` throws it away.
17481    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17482    fn take_optional_paren_size(&mut self) -> Option<u8> {
17483        let Some(Token::Integer(n)) = self
17484            .tokens
17485            .get(self.pos + 1)
17486            .filter(|_| matches!(self.peek(), Token::LParen))
17487            .cloned()
17488        else {
17489            self.consume_optional_paren_size();
17490            return None;
17491        };
17492        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17493            self.consume_optional_paren_size();
17494            return None;
17495        }
17496        self.consume_optional_paren_size();
17497        u8::try_from(n).ok()
17498    }
17499
17500    fn consume_optional_paren_size(&mut self) {
17501        if !matches!(self.peek(), Token::LParen) {
17502            return;
17503        }
17504        self.advance();
17505        // Skip until matching RParen (allow nested or any tokens).
17506        let mut depth = 1usize;
17507        while depth > 0 {
17508            match self.peek() {
17509                Token::LParen => depth += 1,
17510                Token::RParen => depth -= 1,
17511                Token::Eof => return,
17512                _ => {}
17513            }
17514            self.advance();
17515        }
17516    }
17517
17518    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17519        if !matches!(self.peek(), Token::LParen) {
17520            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17521        }
17522        self.advance();
17523        let n = match self.advance() {
17524            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17525                message: format!("{label} size too large: {n}"),
17526                token_pos: self.consumed_pos(),
17527            })?,
17528            other => {
17529                return Err(ParseError {
17530                    message: format!("expected positive integer {label} size, got {other:?}"),
17531                    token_pos: self.consumed_pos(),
17532                });
17533            }
17534        };
17535        if !matches!(self.peek(), Token::RParen) {
17536            return Err(self.err(format!(
17537                "expected ')' after {label} size, got {:?}",
17538                self.peek()
17539            )));
17540        }
17541        self.advance();
17542        Ok(n)
17543    }
17544
17545    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17546    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17547    /// key, like MySQL) whose action skips conflicting rows.
17548    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17549    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17550    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17551    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17552    /// common bulk-upsert spellings —
17553    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17554    ///     REPLACE INTO t SELECT …
17555    /// — were a parse error / a duplicate-key failure respectively.
17556    ///
17557    /// Precedence: an explicitly written clause beats a statement-level flag.
17558    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17559    /// implicit `REPLACE` and `IGNORE` lowerings.
17560    fn parse_insert_conflict_clause(
17561        &mut self,
17562        replace: bool,
17563        ignore: bool,
17564    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17565        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17566            return Ok(Some(c));
17567        }
17568        if let Some(c) = self.parse_optional_on_conflict()? {
17569            return Ok(Some(c));
17570        }
17571        if replace {
17572            // REPLACE INTO = delete-then-insert, which PG spells as
17573            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17574            // reads an empty assignment list as "take the incoming row".
17575            return Ok(Some(crate::ast::OnConflictClause {
17576                target_columns: Vec::new(),
17577                index_where: None,
17578                constraint_name: None,
17579                mysql_lowered: true,
17580                action: crate::ast::OnConflictAction::Update {
17581                    assignments: Vec::new(),
17582                    where_: None,
17583                },
17584            }));
17585        }
17586        if ignore {
17587            return Ok(Some(Self::insert_ignore_clause()));
17588        }
17589        Ok(None)
17590    }
17591
17592    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17593    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17594    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17595    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17596    fn parse_optional_on_duplicate_key(
17597        &mut self,
17598    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17599        if !(matches!(self.peek(), Token::On)
17600            && matches!(self.tokens.get(self.pos + 1),
17601                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17602        {
17603            return Ok(None);
17604        }
17605        self.advance(); // ON
17606        self.advance(); // DUPLICATE
17607        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17608            return Err(self.err(format!(
17609                "expected KEY after ON DUPLICATE, got {:?}",
17610                self.peek()
17611            )));
17612        }
17613        self.advance();
17614        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17615            return Err(self.err(format!(
17616                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17617                self.peek()
17618            )));
17619        }
17620        self.advance();
17621        let mut assignments: Vec<(String, Expr)> = Vec::new();
17622        loop {
17623            let col = self.expect_ident_like()?;
17624            if !matches!(self.peek(), Token::Eq) {
17625                return Err(self.err(format!(
17626                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17627                    self.peek()
17628                )));
17629            }
17630            self.advance();
17631            let mut expr = self.parse_expr(0)?;
17632            Self::rewrite_mysql_values_refs(&mut expr);
17633            assignments.push((col, expr));
17634            if matches!(self.peek(), Token::Comma) {
17635                self.advance();
17636                continue;
17637            }
17638            break;
17639        }
17640        Ok(Some(crate::ast::OnConflictClause {
17641            target_columns: Vec::new(),
17642            index_where: None,
17643            constraint_name: None,
17644            mysql_lowered: true,
17645            action: crate::ast::OnConflictAction::Update {
17646                assignments,
17647                where_: None,
17648            },
17649        }))
17650    }
17651
17652    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17653        crate::ast::OnConflictClause {
17654            target_columns: Vec::new(),
17655            index_where: None,
17656            constraint_name: None,
17657            mysql_lowered: true,
17658            action: crate::ast::OnConflictAction::Nothing,
17659        }
17660    }
17661
17662    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17663        debug_assert!(
17664            matches!(self.peek(), Token::Insert)
17665                || (replace
17666                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17667        );
17668        self.advance();
17669        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17670        // would raise a duplicate-key error instead of failing the statement,
17671        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17672        // plain ident to the lexer; only the MySQL dialect accepts it here.
17673        let ignore = self.mysql_dialect
17674            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17675        if ignore {
17676            self.advance();
17677        }
17678        if !matches!(self.peek(), Token::Into) {
17679            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17680        }
17681        self.advance();
17682        let table = self.expect_ident_like()?;
17683        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17684        // grammar requires the AS keyword here (a bare identifier would be
17685        // ambiguous with a column list). The alias is what the ON CONFLICT
17686        // DO UPDATE expressions refer to the target row by.
17687        let alias = if matches!(self.peek(), Token::As) {
17688            self.advance();
17689            Some(self.expect_ident_like()?)
17690        } else {
17691            None
17692        };
17693        // v7.39 (round 428) — MySQL's SET-form INSERT:
17694        //     INSERT INTO t SET a = 1, b = 'x'
17695        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17696        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17697        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17698        // measured). So it lowers to the column list + one VALUES row and
17699        // rejoins the ordinary path, which already handles every one of
17700        // those. PG has no such spelling, hence the dialect gate.
17701        if self.mysql_dialect
17702            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17703        {
17704            self.advance(); // SET
17705            let mut names = Vec::new();
17706            let mut values = Vec::new();
17707            loop {
17708                names.push(self.expect_ident_like()?);
17709                if !matches!(self.peek(), Token::Eq) {
17710                    return Err(self.err(alloc::format!(
17711                        "expected '=' in INSERT … SET, got {:?}",
17712                        self.peek()
17713                    )));
17714                }
17715                self.advance();
17716                // `SET a = DEFAULT` rides the same `__column_default` marker
17717                // the VALUES-row and UPDATE-SET paths use; the INSERT
17718                // executor resolves it against the target column.
17719                if matches!(self.peek(), Token::Default) {
17720                    self.advance();
17721                    values.push(Expr::FunctionCall {
17722                        name: "__column_default".to_string(),
17723                        args: Vec::new(),
17724                    });
17725                } else {
17726                    values.push(self.parse_expr(0)?);
17727                }
17728                if matches!(self.peek(), Token::Comma) {
17729                    self.advance();
17730                    continue;
17731                }
17732                break;
17733            }
17734            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17735            let returning = self.parse_optional_returning()?;
17736            return Ok(Statement::Insert(InsertStatement {
17737                ctes: Vec::new(),
17738                table,
17739                alias,
17740                columns: Some(names),
17741                rows: alloc::vec![values],
17742                select_source: None,
17743                // MySQL's SET form has no `OVERRIDING …` clause (that is
17744                // PG's identity-column spelling).
17745                overriding: Overriding::None,
17746                mysql_ignore: ignore,
17747                on_conflict,
17748                returning,
17749            }));
17750        }
17751        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17752        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17753        // a parenthesized query source instead (PG select_with_parens:
17754        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17755        // both keywords are reserved in PG, so no column list can start
17756        // with them.
17757        let columns = if matches!(self.peek(), Token::LParen) {
17758            self.advance();
17759            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17760                let select_stmt = if self.peek_is_with_kw() {
17761                    self.advance();
17762                    self.parse_nested_with_select()?
17763                } else {
17764                    match self.parse_select_stmt()? {
17765                        Statement::Select(s) => s,
17766                        other => {
17767                            return Err(self.err(alloc::format!(
17768                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17769                            )));
17770                        }
17771                    }
17772                };
17773                if !matches!(self.peek(), Token::RParen) {
17774                    return Err(self.err(format!(
17775                        "expected ')' after parenthesized INSERT source, got {:?}",
17776                        self.peek()
17777                    )));
17778                }
17779                self.advance();
17780                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17781                let returning = self.parse_optional_returning()?;
17782                return Ok(Statement::Insert(InsertStatement {
17783                    ctes: Vec::new(),
17784                    table,
17785                    alias: alias.clone(),
17786                    columns: None,
17787                    rows: Vec::new(),
17788                    select_source: Some(Box::new(select_stmt)),
17789                    on_conflict,
17790                    returning,
17791                    overriding: Overriding::None,
17792                    mysql_ignore: ignore,
17793                }));
17794            }
17795            let mut names = Vec::new();
17796            loop {
17797                names.push(self.expect_ident_like()?);
17798                match self.peek() {
17799                    Token::Comma => {
17800                        self.advance();
17801                    }
17802                    Token::RParen => {
17803                        self.advance();
17804                        break;
17805                    }
17806                    other => {
17807                        return Err(self.err(format!(
17808                            "expected ',' or ')' in INSERT column list, got {other:?}"
17809                        )));
17810                    }
17811                }
17812            }
17813            Some(names)
17814        } else {
17815            None
17816        };
17817        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17818        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17819        // is captured on the statement so the engine can apply PG's
17820        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17821        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17822        {
17823            self.advance();
17824            let which = self.expect_ident_like()?;
17825            let ov = if which.eq_ignore_ascii_case("system") {
17826                Overriding::System
17827            } else if which.eq_ignore_ascii_case("user") {
17828                Overriding::User
17829            } else {
17830                return Err(self.err(format!(
17831                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17832                )));
17833            };
17834            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17835                return Err(self.err(format!(
17836                    "expected VALUE after OVERRIDING {}, got {:?}",
17837                    which.to_ascii_uppercase(),
17838                    self.peek()
17839                )));
17840            }
17841            self.advance();
17842            ov
17843        } else {
17844            Overriding::None
17845        };
17846        // `INSERT INTO t DEFAULT VALUES` — a single row made
17847        // entirely of column defaults. Lower to the permuted
17848        // column-list path with an empty list: every schema column
17849        // is unmapped, so the engine fills each from its default
17850        // (serials advance, plain defaults evaluate, the rest NULL).
17851        if matches!(self.peek(), Token::Default) {
17852            self.advance();
17853            if !matches!(self.peek(), Token::Values) {
17854                return Err(self.err(format!(
17855                    "expected VALUES after DEFAULT in INSERT, got {:?}",
17856                    self.peek()
17857                )));
17858            }
17859            self.advance();
17860            if columns.is_some() {
17861                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17862            }
17863            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17864            let returning = self.parse_optional_returning()?;
17865            return Ok(Statement::Insert(InsertStatement {
17866                ctes: Vec::new(),
17867                table,
17868                alias: alias.clone(),
17869                columns: Some(Vec::new()),
17870                rows: alloc::vec![Vec::new()],
17871                select_source: None,
17872                on_conflict,
17873                returning,
17874                overriding,
17875                mysql_ignore: ignore,
17876            }));
17877        }
17878        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17879        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17880        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17881        // SELECT …`) heads the SOURCE select, as in PG (the statement's
17882        // own WITH comes before INSERT).
17883        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17884            let select_stmt = if self.peek_is_with_kw() {
17885                self.advance();
17886                self.parse_nested_with_select()?
17887            } else {
17888                match self.parse_select_stmt()? {
17889                    Statement::Select(s) => s,
17890                    other => {
17891                        return Err(self.err(alloc::format!(
17892                            "expected SELECT after INSERT INTO ... target, got {other:?}"
17893                        )));
17894                    }
17895                }
17896            };
17897            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17898            let returning = self.parse_optional_returning()?;
17899            return Ok(Statement::Insert(InsertStatement {
17900                ctes: Vec::new(),
17901                table,
17902                alias: alias.clone(),
17903                columns,
17904                rows: Vec::new(),
17905                select_source: Some(Box::new(select_stmt)),
17906                on_conflict,
17907                returning,
17908                overriding,
17909                mysql_ignore: ignore,
17910            }));
17911        }
17912        if !matches!(self.peek(), Token::Values) {
17913            return Err(self.err(format!(
17914                "expected VALUES or SELECT after table name, got {:?}",
17915                self.peek()
17916            )));
17917        }
17918        self.advance();
17919        if !matches!(self.peek(), Token::LParen) {
17920            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17921        }
17922        let mut rows = Vec::new();
17923        loop {
17924            // Each iteration consumes one `(expr, expr, …)` tuple.
17925            if !matches!(self.peek(), Token::LParen) {
17926                return Err(self.err(format!(
17927                    "expected '(' for next VALUES tuple, got {:?}",
17928                    self.peek()
17929                )));
17930            }
17931            self.advance();
17932            let mut tuple = Vec::new();
17933            loop {
17934                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17935                // the column's declared default for that slot. Rides out as the
17936                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17937                // path uses; the INSERT executor resolves it per target column.
17938                if matches!(self.peek(), Token::Default) {
17939                    self.advance();
17940                    tuple.push(Expr::FunctionCall {
17941                        name: "__column_default".to_string(),
17942                        args: Vec::new(),
17943                    });
17944                } else {
17945                    tuple.push(self.parse_expr(0)?);
17946                }
17947                match self.peek() {
17948                    Token::Comma => {
17949                        self.advance();
17950                    }
17951                    Token::RParen => {
17952                        self.advance();
17953                        break;
17954                    }
17955                    other => {
17956                        return Err(self.err(format!(
17957                            "expected ',' or ')' in VALUES tuple, got {other:?}"
17958                        )));
17959                    }
17960                }
17961            }
17962            if tuple.is_empty() {
17963                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17964            }
17965            rows.push(tuple);
17966            // Continue with comma-separated tuples.
17967            if matches!(self.peek(), Token::Comma) {
17968                self.advance();
17969            } else {
17970                break;
17971            }
17972        }
17973        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17974        // to ON CONFLICT DO UPDATE with an empty conflict target
17975        // (the engine picks the table's first unique index, which
17976        // matches MySQL's any-unique-key behaviour for the common
17977        // single-key case). `VALUES(col)` in the assignments is
17978        // MySQL's spelling of EXCLUDED.col.
17979        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17980        let returning = self.parse_optional_returning()?;
17981        Ok(Statement::Insert(InsertStatement {
17982            ctes: Vec::new(),
17983            table,
17984            alias,
17985            columns,
17986            rows,
17987            select_source: None,
17988            on_conflict,
17989            returning,
17990            overriding,
17991            mysql_ignore: ignore,
17992        }))
17993    }
17994
17995    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
17996    /// the incoming row's value — exactly PG's EXCLUDED.col.
17997    fn rewrite_mysql_values_refs(e: &mut Expr) {
17998        match e {
17999            Expr::FunctionCall { name, args }
18000                if name.eq_ignore_ascii_case("values")
18001                    && args.len() == 1
18002                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18003            {
18004                let Expr::Column(c) = &args[0] else {
18005                    unreachable!("guarded above");
18006                };
18007                *e = Expr::Column(crate::ast::ColumnName {
18008                    qualifier: Some("EXCLUDED".to_string()),
18009                    name: c.name.clone(),
18010                });
18011            }
18012            Expr::FunctionCall { args, .. } => {
18013                for a in args {
18014                    Self::rewrite_mysql_values_refs(a);
18015                }
18016            }
18017            Expr::Binary { lhs, rhs, .. } => {
18018                Self::rewrite_mysql_values_refs(lhs);
18019                Self::rewrite_mysql_values_refs(rhs);
18020            }
18021            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18022                Self::rewrite_mysql_values_refs(expr);
18023            }
18024            Expr::Case {
18025                operand,
18026                branches,
18027                else_branch,
18028            } => {
18029                if let Some(op) = operand {
18030                    Self::rewrite_mysql_values_refs(op);
18031                }
18032                for (w, t) in branches {
18033                    Self::rewrite_mysql_values_refs(w);
18034                    Self::rewrite_mysql_values_refs(t);
18035                }
18036                if let Some(el) = else_branch {
18037                    Self::rewrite_mysql_values_refs(el);
18038                }
18039            }
18040            _ => {}
18041        }
18042    }
18043
18044    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18045    /// clause sitting between the INSERT body and the trailing
18046    /// RETURNING. All keywords come in as bare idents; `ON` is
18047    /// a reserved Token though.
18048    fn parse_optional_on_conflict(
18049        &mut self,
18050    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18051        if !matches!(self.peek(), Token::On) {
18052            return Ok(None);
18053        }
18054        // Peek further: we want exactly "ON CONFLICT ...". If the
18055        // next ident isn't "conflict", let some other parser handle.
18056        let next_is_conflict = matches!(
18057            self.tokens.get(self.pos + 1),
18058            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18059        );
18060        if !next_is_conflict {
18061            return Ok(None);
18062        }
18063        self.advance(); // ON
18064        self.advance(); // CONFLICT
18065        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18066        // the constraint instead of listing columns (the pg_dump
18067        // form); the engine resolves it.
18068        let mut constraint_name: Option<String> = None;
18069        if matches!(self.peek(), Token::On) {
18070            self.advance(); // ON
18071            match self.advance() {
18072                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18073                }
18074                other => {
18075                    return Err(self.err(alloc::format!(
18076                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18077                    )));
18078                }
18079            }
18080            constraint_name = Some(self.expect_ident_like()?);
18081        }
18082        // Optional `(col [, col]*)` target list.
18083        let mut target_columns: Vec<String> = Vec::new();
18084        if matches!(self.peek(), Token::LParen) {
18085            self.advance();
18086            loop {
18087                target_columns.push(self.expect_ident_like()?);
18088                match self.peek() {
18089                    Token::Comma => {
18090                        self.advance();
18091                    }
18092                    Token::RParen => {
18093                        self.advance();
18094                        break;
18095                    }
18096                    other => {
18097                        return Err(self.err(alloc::format!(
18098                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18099                        )));
18100                    }
18101                }
18102            }
18103        }
18104        // v7.39 (round 240) — optional index predicate after the target
18105        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18106        // PARTIAL unique index; SPG's arbiters are full indexes, which
18107        // satisfy any predicate, so it is parsed and carried but not
18108        // consulted (recorded residual: partial-unique-index arbiters).
18109        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18110            self.advance();
18111            Some(self.parse_expr(0)?)
18112        } else {
18113            None
18114        };
18115        // Required `DO`.
18116        match self.advance() {
18117            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18118            other => {
18119                return Err(self.err(alloc::format!(
18120                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18121                )));
18122            }
18123        }
18124        // Action: NOTHING | UPDATE SET …
18125        let action = match self.advance() {
18126            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18127                crate::ast::OnConflictAction::Nothing
18128            }
18129            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18130                self.parse_on_conflict_update_action()?
18131            }
18132            other => {
18133                return Err(self.err(alloc::format!(
18134                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18135                )));
18136            }
18137        };
18138        Ok(Some(crate::ast::OnConflictClause {
18139            target_columns,
18140            index_where,
18141            constraint_name,
18142            mysql_lowered: false,
18143            action,
18144        }))
18145    }
18146
18147    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18148    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18149    /// consumed `UPDATE`.
18150    fn parse_on_conflict_update_action(
18151        &mut self,
18152    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18153        // `SET`
18154        match self.advance() {
18155            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18156            other => {
18157                return Err(self.err(alloc::format!(
18158                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18159                )));
18160            }
18161        }
18162        let mut assignments: Vec<(String, Expr)> = Vec::new();
18163        loop {
18164            let col = self.expect_ident_like()?;
18165            if !matches!(self.peek(), Token::Eq) {
18166                return Err(self.err(alloc::format!(
18167                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18168                    self.peek()
18169                )));
18170            }
18171            self.advance();
18172            let value = self.parse_expr(0)?;
18173            assignments.push((col, value));
18174            if matches!(self.peek(), Token::Comma) {
18175                self.advance();
18176                continue;
18177            }
18178            break;
18179        }
18180        let where_ = if matches!(self.peek(), Token::Where) {
18181            self.advance();
18182            Some(self.parse_expr(0)?)
18183        } else {
18184            None
18185        };
18186        Ok(crate::ast::OnConflictAction::Update {
18187            assignments,
18188            where_,
18189        })
18190    }
18191
18192    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18193        let mut items = Vec::new();
18194        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18195        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18196        // answers one zero-column row per row of t, and a bare `SELECT`
18197        // answers a single zero-column row. SPG required at least one
18198        // item, so both were syntax errors. Recognised by the token that
18199        // follows — nothing that can start an expression appears here.
18200        if self.select_list_is_empty_here() {
18201            return Ok(items);
18202        }
18203        loop {
18204            items.push(self.parse_select_item()?);
18205            if matches!(self.peek(), Token::Comma) {
18206                self.advance();
18207            } else {
18208                break;
18209            }
18210        }
18211        Ok(items)
18212    }
18213
18214    /// Is the target list empty at this point — i.e. does the next token
18215    /// end the SELECT's item list rather than start an item?
18216    fn select_list_is_empty_here(&self) -> bool {
18217        match self.peek() {
18218            Token::From
18219            | Token::Where
18220            | Token::Group
18221            | Token::Having
18222            | Token::Order
18223            | Token::Limit
18224            | Token::Offset
18225            | Token::Semicolon
18226            | Token::RParen
18227            | Token::Union
18228            | Token::Except
18229            | Token::Eof => true,
18230            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18231            // with unreserved keywords, so they arrive as plain idents.
18232            Token::Ident(s) => {
18233                s.eq_ignore_ascii_case("fetch")
18234                    || s.eq_ignore_ascii_case("window")
18235                    || s.eq_ignore_ascii_case("intersect")
18236            }
18237            _ => false,
18238        }
18239    }
18240
18241    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18242        if matches!(self.peek(), Token::Star) {
18243            self.advance();
18244            return Ok(SelectItem::Wildcard);
18245        }
18246        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18247        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18248        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18249        // `<ident> . *` with nothing binding tighter.
18250        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18251            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18252                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18253            {
18254                self.advance(); // qualifier
18255                self.advance(); // .
18256                self.advance(); // *
18257                return Ok(SelectItem::QualifiedWildcard(q));
18258            }
18259        }
18260        let start_tok = self.pos;
18261        let expr = self.parse_expr(0)?;
18262        let end_tok = self.consumed_pos();
18263        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18264        // multi-column function returns into columns. Marked here and lowered in
18265        // `parse_bare_select`, where the FROM clause is in hand.
18266        if matches!(self.peek(), Token::Dot)
18267            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18268        {
18269            self.advance(); // .
18270            self.advance(); // *
18271            return Ok(SelectItem::Expr {
18272                expr: Expr::FunctionCall {
18273                    name: "__record_expand".to_string(),
18274                    args: alloc::vec![expr],
18275                },
18276                alias: None,
18277            });
18278        }
18279        let alias = match self.parse_optional_alias()? {
18280            Some(a) => Some(a),
18281            None => self.mysql_item_label(&expr, start_tok, end_tok),
18282        };
18283        Ok(SelectItem::Expr { expr, alias })
18284    }
18285
18286    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18287    /// carries no `AS`, filled in here so every downstream path reports it
18288    /// without knowing the rule. `None` leaves the item un-aliased, which is
18289    /// what a PG session always gets.
18290    ///
18291    /// Measured against MariaDB 11, three rules and no more:
18292    ///
18293    /// | item             | label      | why                          |
18294    /// |------------------|------------|------------------------------|
18295    /// | `lbl.a`          | `a`        | a column reports its name    |
18296    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18297    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18298    ///
18299    /// The third is why this lives in the parser at all: the label is the
18300    /// text the client WROTE, down to the spacing, so it cannot be printed
18301    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18302    ///
18303    /// Comments survive, and that is right: through a `mariadb` CLI both
18304    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18305    /// CLIENT stripping the comment before it sends. Asked over the raw
18306    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18307    /// produces.
18308    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18309        if !self.mysql_dialect {
18310            return None;
18311        }
18312        match expr {
18313            // A column already reports its own name downstream; naming it
18314            // again here would only re-state the qualifier the label drops.
18315            Expr::Column(_) => None,
18316            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18317            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18318        }
18319    }
18320
18321    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18322    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18323    /// with PG's default column1..columnN names; subsequent rows
18324    /// chain as UNION ALL peers. Shared by the FROM-position
18325    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18326    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18327        let mut row_selects: Vec<SelectStatement> = Vec::new();
18328        loop {
18329            if !matches!(self.peek(), Token::LParen) {
18330                return Err(self.err(alloc::format!(
18331                    "expected '(' to start a VALUES row, got {:?}",
18332                    self.peek()
18333                )));
18334            }
18335            self.advance(); // (
18336            let mut items: Vec<SelectItem> = Vec::new();
18337            loop {
18338                let expr = self.parse_expr(0)?;
18339                items.push(SelectItem::Expr {
18340                    expr,
18341                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18342                });
18343                match self.peek() {
18344                    Token::Comma => {
18345                        self.advance();
18346                    }
18347                    Token::RParen => break,
18348                    other => {
18349                        return Err(self.err(alloc::format!(
18350                            "expected ',' or ')' in VALUES row, got {other:?}"
18351                        )));
18352                    }
18353                }
18354            }
18355            self.advance(); // )
18356            row_selects.push(SelectStatement {
18357                locking: None,
18358                ctes: Vec::new(),
18359                distinct: false,
18360                distinct_on: Vec::new(),
18361                items,
18362                from: None,
18363                where_: None,
18364                group_by: None,
18365                group_by_all: false,
18366                having: None,
18367                unions: Vec::new(),
18368                order_by: Vec::new(),
18369                limit: None,
18370                offset: None,
18371                limit_with_ties: false,
18372                window_check_exprs: Vec::new(),
18373            });
18374            if matches!(self.peek(), Token::Comma) {
18375                self.advance();
18376                continue;
18377            }
18378            break;
18379        }
18380        let mut head = row_selects.remove(0);
18381        head.unions = row_selects
18382            .into_iter()
18383            .map(|s| (UnionKind::All, s))
18384            .collect();
18385        Ok(head)
18386    }
18387
18388    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18389        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18390        // children. It was read as a table NAMED `only`, so the query
18391        // failed on `relation "only" does not exist`.
18392        //
18393        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18394        // absorbed the keyword, reasoning that SPG's children are
18395        // separate relations a plain scan does not descend into, so ONLY
18396        // already described the scan. That stopped being true when a
18397        // partition parent started unioning its children: measured,
18398        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18399        // where PG answers 0. The flag is carried now.
18400        let mut only = false;
18401        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18402            && matches!(
18403                self.tokens.get(self.pos + 1),
18404                Some(Token::Ident(_) | Token::QuotedIdent(_))
18405            )
18406        {
18407            only = true;
18408            self.advance();
18409        }
18410        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18411        // for these SRFs the keyword is noise at parse time: the
18412        // join executor already substitutes outer-column references
18413        // into unnest_expr / generate_series_args per outer row
18414        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18415        // licences the correlation even without the keyword. Absorb
18416        // it and fall through to the SRF arms below.
18417        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18418        // just the four builtin SRFs: a user set-returning function on a JOIN's
18419        // right side is the whole point of LATERAL. The keyword stays noise at
18420        // parse time — the join executor substitutes the outer row into the
18421        // call's arguments per outer row.
18422        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18423            && matches!(
18424                self.tokens.get(self.pos + 1),
18425                // The json_each family has its OWN `LATERAL …` arm below, which
18426                // needs to see the keyword — absorbing it here would send those
18427                // calls down the generic table-function channel instead.
18428                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18429            )
18430            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18431        {
18432            self.advance(); // LATERAL
18433        }
18434        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18435        // set-returning function whose argument may reference a
18436        // preceding FROM item. We rewrite this to
18437        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18438        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18439        // executor handles per-outer-row evaluation and the
18440        // SRF-primary jsonb_each_text path handles the inner
18441        // materialisation. Sentori 0067 backfill is the dogfood
18442        // shape.
18443        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18444            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18445            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18446        {
18447            self.advance(); // LATERAL
18448            let each_fn = match self.peek() {
18449                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18450                _ => unreachable!(),
18451            };
18452            self.advance(); // jsonb_each[_text] / json_each[_text]
18453            self.advance(); // (
18454            let arg = self.parse_expr(0)?;
18455            if !matches!(self.peek(), Token::RParen) {
18456                return Err(self.err(alloc::format!(
18457                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18458                    self.peek()
18459                )));
18460            }
18461            self.advance();
18462            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18463            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18464            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18465            //               FROM jsonb_each_text(<arg>) AS __srf__
18466            // PG's `AS kv(key, value)` column-alias list maps
18467            // positions to names; default to (key, value) when
18468            // omitted (matching the SRF's natural column names).
18469            let srf_alias = "__srf__".to_string();
18470            let key_alias = column_aliases
18471                .first()
18472                .cloned()
18473                .unwrap_or_else(|| "key".to_string());
18474            let value_alias = column_aliases
18475                .get(1)
18476                .cloned()
18477                .unwrap_or_else(|| "value".to_string());
18478            let inner_select = crate::ast::SelectStatement {
18479                locking: None,
18480                ctes: Vec::new(),
18481                distinct: false,
18482                distinct_on: Vec::new(),
18483                items: alloc::vec![
18484                    crate::ast::SelectItem::Expr {
18485                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18486                            qualifier: Some(srf_alias.clone()),
18487                            name: "key".to_string(),
18488                        }),
18489                        alias: Some(key_alias),
18490                    },
18491                    crate::ast::SelectItem::Expr {
18492                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18493                            qualifier: Some(srf_alias.clone()),
18494                            name: "value".to_string(),
18495                        }),
18496                        alias: Some(value_alias),
18497                    },
18498                ],
18499                from: Some(crate::ast::FromClause {
18500                    primary: TableRef {
18501                        name: srf_alias.clone(),
18502                        alias: Some(srf_alias.clone()),
18503                        only: false,
18504                        as_of_segment: None,
18505                        unnest_expr: None,
18506                        unnest_column_aliases: Vec::new(),
18507                        with_ordinality: false,
18508                        generate_series_args: None,
18509                        lateral_subquery: None,
18510                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18511                        table_fn_call: None,
18512                        rows_from: None,
18513                        json_table: None,
18514                        scalar_fn_item: false,
18515                    },
18516                    joins: Vec::new(),
18517                }),
18518                where_: None,
18519                group_by: None,
18520                group_by_all: false,
18521                having: None,
18522                unions: Vec::new(),
18523                order_by: Vec::new(),
18524                limit: None,
18525                offset: None,
18526                limit_with_ties: false,
18527                window_check_exprs: Vec::new(),
18528            };
18529            return Ok(TableRef {
18530                name: alias.clone(),
18531                alias: Some(alias),
18532                only: false,
18533                as_of_segment: None,
18534                unnest_expr: None,
18535                unnest_column_aliases: Vec::new(),
18536                with_ordinality: false,
18537                generate_series_args: None,
18538                lateral_subquery: Some(Box::new(inner_select)),
18539                jsonb_each_text_arg: None,
18540                table_fn_call: None,
18541                rows_from: None,
18542                json_table: None,
18543                scalar_fn_item: false,
18544            });
18545        }
18546        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18547        // without an explicit `LATERAL` keyword is the same shape
18548        // PG accepts (SRF naturally licences lateral correlation).
18549        // We mirror the LATERAL rewrite when the argument syntactic-
18550        // ally references an outer column (Column { qualifier:
18551        // Some(_), … }). For simplicity we apply the rewrite
18552        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18553        // in the FROM-list — caller-side join parsing positions
18554        // this peek correctly.
18555        // (Implementation note: detection lives below; the LATERAL
18556        // branch above already covers the explicit form; the bare
18557        // form falls through to the plain SRF arm and the engine
18558        // treats it as a constant-arg SRF if no outer reference is
18559        // present.)
18560        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18561        // table. Detect at the head so it claims precedence over
18562        // every other table-ref shape (unnest / generate_series /
18563        // bare ident); the lateral subquery itself follows the
18564        // regular SELECT grammar.
18565        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18566        // t(cols)`. Each row lowers to a constant SELECT with PG's
18567        // default column1..columnN names; subsequent rows chain as
18568        // UNION ALL peers. The result rides the derived-table
18569        // lateral_subquery channel — zero executor work.
18570        if matches!(self.peek(), Token::LParen)
18571            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18572        {
18573            self.advance(); // (
18574            self.advance(); // VALUES
18575            let head = self.parse_values_rows_body()?;
18576            if !matches!(self.peek(), Token::RParen) {
18577                return Err(self.err(alloc::format!(
18578                    "expected ')' after VALUES list, got {:?}",
18579                    self.peek()
18580                )));
18581            }
18582            self.advance();
18583            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18584            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18585            return Ok(TableRef {
18586                name,
18587                alias: alias_ident,
18588                only: false,
18589                as_of_segment: None,
18590                unnest_expr: None,
18591                unnest_column_aliases: column_aliases,
18592                with_ordinality: false,
18593                generate_series_args: None,
18594                lateral_subquery: Some(Box::new(head)),
18595                jsonb_each_text_arg: None,
18596                table_fn_call: None,
18597                rows_from: None,
18598                json_table: None,
18599                scalar_fn_item: false,
18600            });
18601        }
18602        // v7.37.17 (17.6 siblings) — plain derived table:
18603        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18604        // lateral_subquery channel the explicit LATERAL form uses —
18605        // an uncorrelated inner SELECT executes identically. The
18606        // inner parse carries UNION tails (they live on
18607        // SelectStatement.unions).
18608        // v7.37 D.20 — the derived-table inner may itself be a
18609        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18610        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18611        // bare `(SELECT …)`. parse_one_statement already routes a leading
18612        // `(` set-op group (its LParen arm) and a leading WITH
18613        // (parse_with_cte_then_select), so widen the second-token gate to
18614        // Select | LParen | WITH.
18615        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18616        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18617        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18618        // has existed since the shorthand landed and `parse_bare_select`
18619        // already routes it ("valid anywhere a SELECT head is"); what was
18620        // missing is this second-token gate, and the CTE body's dispatch
18621        // below. Round 868 found both by putting the shorthand in a
18622        // subquery — the top-level forms had been the only ones tested.
18623        if matches!(self.peek(), Token::LParen)
18624            && (matches!(
18625                self.tokens.get(self.pos + 1),
18626                Some(Token::Select | Token::LParen | Token::Table)
18627            ) || matches!(self.tokens.get(self.pos + 1),
18628                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18629        {
18630            self.advance(); // (
18631            let inner = match self.parse_one_statement()? {
18632                Statement::Select(s) => s,
18633                other => {
18634                    return Err(self.err(alloc::format!(
18635                        "expected SELECT inside derived table ( … ), got {other:?}"
18636                    )));
18637                }
18638            };
18639            if !matches!(self.peek(), Token::RParen) {
18640                return Err(self.err(alloc::format!(
18641                    "expected ')' after derived-table subquery, got {:?}",
18642                    self.peek()
18643                )));
18644            }
18645            self.advance();
18646            // `AS t(a, b)` column-alias list rides the
18647            // unnest_column_aliases field (same positional-rename
18648            // contract the unnest SRFs use).
18649            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18650            let name = alias_ident
18651                .clone()
18652                .unwrap_or_else(|| "subquery".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(inner)),
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        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18671            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18672        {
18673            self.advance(); // LATERAL
18674            self.advance(); // (
18675            // Parse the inner SELECT.
18676            let inner = match self.parse_one_statement()? {
18677                Statement::Select(s) => s,
18678                other => {
18679                    return Err(self.err(alloc::format!(
18680                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18681                    )));
18682                }
18683            };
18684            if !matches!(self.peek(), Token::RParen) {
18685                return Err(self.err(alloc::format!(
18686                    "expected ')' after LATERAL subquery, got {:?}",
18687                    self.peek()
18688                )));
18689            }
18690            self.advance();
18691            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18692            // `(VALUES …) t(g)` derived table round-trips through view-body
18693            // Display, which renders on the lateral_subquery channel).
18694            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18695            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18696            return Ok(TableRef {
18697                name,
18698                alias: alias_ident,
18699                only: false,
18700                as_of_segment: None,
18701                unnest_expr: None,
18702                unnest_column_aliases: column_aliases,
18703                with_ordinality: false,
18704                generate_series_args: None,
18705                lateral_subquery: Some(Box::new(inner)),
18706                jsonb_each_text_arg: None,
18707                table_fn_call: None,
18708                rows_from: None,
18709                json_table: None,
18710                scalar_fn_item: false,
18711            });
18712        }
18713        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18714        // function as a FROM item. Emits one row per (key, value)
18715        // pair in the JSONB object argument as TEXT columns. May
18716        // be wrapped in CROSS JOIN LATERAL when the argument
18717        // references a preceding FROM item (sentori migration
18718        // 0067 backfill shape: `CROSS JOIN LATERAL
18719        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18720        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18721            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18722        {
18723            let each_fn = match self.peek() {
18724                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18725                _ => unreachable!(),
18726            };
18727            self.advance(); // jsonb_each[_text] / json_each[_text]
18728            self.advance(); // (
18729            let arg = self.parse_expr(0)?;
18730            if !matches!(self.peek(), Token::RParen) {
18731                return Err(self.err(alloc::format!(
18732                    "expected ')' after {each_fn}() argument, got {:?}",
18733                    self.peek()
18734                )));
18735            }
18736            self.advance();
18737            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18738            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18739            return Ok(TableRef {
18740                name,
18741                alias: alias_ident,
18742                only: false,
18743                as_of_segment: None,
18744                unnest_expr: None,
18745                // `AS t(k, v)` renames key/value positionally, same as the
18746                // LATERAL-position form already does.
18747                unnest_column_aliases: column_aliases,
18748                with_ordinality: false,
18749                generate_series_args: None,
18750                lateral_subquery: None,
18751                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18752                table_fn_call: None,
18753                rows_from: None,
18754                json_table: None,
18755                scalar_fn_item: false,
18756            });
18757        }
18758        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18759        // (+ json_ variants) — record-returning JSON functions with a
18760        // column-definition list. Desugar to a derived table that
18761        // projects each declared column from the JSON via `->>` + a cast,
18762        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18763        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18764            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18765        {
18766            return self.parse_json_to_record_from();
18767        }
18768        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18769        // row is a text[] of capture groups, so it cannot desugar to unnest
18770        // (that would flatten the array). Wrap it as a derived table
18771        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18772        // SRF path already emits one text[] row per match. PG names the column
18773        // `regexp_matches`; an `AS a(col)` alias overrides it.
18774        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18775                if s.eq_ignore_ascii_case("regexp_matches"))
18776            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18777        {
18778            self.advance(); // fn name
18779            self.advance(); // (
18780            let mut fn_args: Vec<Expr> = Vec::new();
18781            loop {
18782                fn_args.push(self.parse_expr(0)?);
18783                if matches!(self.peek(), Token::Comma) {
18784                    self.advance();
18785                    continue;
18786                }
18787                break;
18788            }
18789            if !matches!(self.peek(), Token::RParen) {
18790                return Err(self.err(alloc::format!(
18791                    "expected ')' after regexp_matches() arguments, got {:?}",
18792                    self.peek()
18793                )));
18794            }
18795            self.advance();
18796            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18797            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18798            // it, so it died on the `with` token while every other table function
18799            // accepted it.
18800            let with_ordinality = self.absorb_with_ordinality();
18801            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18802            let table_alias = alias_ident
18803                .clone()
18804                .unwrap_or_else(|| "regexp_matches".to_string());
18805            // PG names a single-column function's output column after the ALIAS
18806            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18807            // `m` reads as that column and not as a whole-row composite. Naming
18808            // it after the function regardless made `SELECT m[1] FROM … AS m`
18809            // subscript a record.
18810            let col_name = column_aliases
18811                .first()
18812                .cloned()
18813                .or_else(|| alias_ident.clone())
18814                .unwrap_or_else(|| "regexp_matches".to_string());
18815            let inner = crate::ast::SelectStatement {
18816                locking: None,
18817                ctes: Vec::new(),
18818                distinct: false,
18819                distinct_on: Vec::new(),
18820                items: alloc::vec![SelectItem::Expr {
18821                    expr: Expr::FunctionCall {
18822                        name: "regexp_matches".to_string(),
18823                        args: fn_args,
18824                    },
18825                    alias: Some(col_name),
18826                }],
18827                from: None,
18828                where_: None,
18829                group_by: None,
18830                group_by_all: false,
18831                having: None,
18832                unions: Vec::new(),
18833                order_by: Vec::new(),
18834                limit: None,
18835                offset: None,
18836                limit_with_ties: false,
18837                window_check_exprs: Vec::new(),
18838            };
18839            return Ok(TableRef {
18840                name: table_alias.clone(),
18841                alias: Some(table_alias),
18842                only: false,
18843                as_of_segment: None,
18844                unnest_expr: None,
18845                unnest_column_aliases: column_aliases,
18846                with_ordinality,
18847                generate_series_args: None,
18848                lateral_subquery: Some(Box::new(inner)),
18849                jsonb_each_text_arg: None,
18850                table_fn_call: None,
18851                rows_from: None,
18852                json_table: None,
18853                // regexp_matches returns text[], a base type: `SELECT m FROM
18854                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18855                scalar_fn_item: true,
18856            });
18857        }
18858        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18859        // / json_ variants as a FROM item. Rewritten into
18860        // `unnest(<same fn>(<expr>))`: the scalar form returns the
18861        // elements as a TEXT array, and the existing unnest SRF path
18862        // materialises one row per element. PG's natural column name
18863        // is `value`; an `AS a(col)` column-alias list overrides it.
18864        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18865                if s.eq_ignore_ascii_case("jsonb_array_elements")
18866                    || s.eq_ignore_ascii_case("json_array_elements")
18867                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18868                    || s.eq_ignore_ascii_case("json_array_elements_text")
18869                    || s.eq_ignore_ascii_case("jsonb_object_keys")
18870                    || s.eq_ignore_ascii_case("json_object_keys")
18871                    || s.eq_ignore_ascii_case("jsonb_path_query")
18872                    || s.eq_ignore_ascii_case("json_path_query")
18873                    || s.eq_ignore_ascii_case("generate_subscripts")
18874                    || s.eq_ignore_ascii_case("string_to_table")
18875                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
18876            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18877        {
18878            let fn_name = match self.peek() {
18879                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18880                _ => unreachable!(),
18881            };
18882            self.advance(); // fn name
18883            self.advance(); // (
18884            let mut fn_args: Vec<Expr> = Vec::new();
18885            loop {
18886                fn_args.push(self.parse_expr(0)?);
18887                if matches!(self.peek(), Token::Comma) {
18888                    self.advance();
18889                    continue;
18890                }
18891                break;
18892            }
18893            if !matches!(self.peek(), Token::RParen) {
18894                return Err(self.err(alloc::format!(
18895                    "expected ')' after {fn_name}() arguments, got {:?}",
18896                    self.peek()
18897                )));
18898            }
18899            self.advance();
18900            let with_ordinality = self.absorb_with_ordinality();
18901            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18902            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18903            // PG's natural column name: the array-elements SRFs
18904            // declare an OUT parameter `value`; jsonb_object_keys
18905            // and generate_subscripts have none, so the column is
18906            // named after the function. A bare table alias on a
18907            // single-column SRF renames the column too (PG: `FROM
18908            // generate_subscripts(a, 1) AS s` projects column s) —
18909            // except for the OUT-parameter SRFs, whose column stays
18910            // `value` under a bare alias.
18911            let natural_col = if fn_name.ends_with("_array_elements")
18912                || fn_name.ends_with("_array_elements_text")
18913            {
18914                "value".to_string()
18915            } else {
18916                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18917            };
18918            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18919            // Keep any further entries — the second names the
18920            // ordinality column under WITH ORDINALITY.
18921            srf_cols.extend(column_aliases.into_iter().skip(1));
18922            // The *_to_table SRFs are row-streams over the existing
18923            // *_to_array scalars — map the call target; the display
18924            // name (alias / column defaults) keeps the SRF spelling.
18925            let call_name = match fn_name.as_str() {
18926                "string_to_table" => "string_to_array".to_string(),
18927                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18928                _ => fn_name,
18929            };
18930            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18931            // preceding FROM item (bare or qualified column) is correlated;
18932            // route it through the per-outer-row lateral channel.
18933            let expr = crate::ast::Expr::FunctionCall {
18934                name: call_name,
18935                args: fn_args,
18936            };
18937            let correlated = Self::expr_has_any_column(&expr);
18938            let tref = TableRef {
18939                name,
18940                alias: alias_ident,
18941                only: false,
18942                as_of_segment: None,
18943                unnest_expr: Some(Box::new(expr)),
18944                unnest_column_aliases: srf_cols,
18945                with_ordinality,
18946                generate_series_args: None,
18947                lateral_subquery: None,
18948                jsonb_each_text_arg: None,
18949                table_fn_call: None,
18950                rows_from: None,
18951                json_table: None,
18952                // Each of these returns a BASE type (jsonb / text / int), so the item's
18953                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18954                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18955                scalar_fn_item: !with_ordinality,
18956            };
18957            return Ok(if correlated {
18958                Self::wrap_correlated_srf(tref)
18959            } else {
18960                tref
18961            });
18962        }
18963        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18964        // explicit parallel-zip syntax. Each entry lowers to its
18965        // array-returning scalar form (unnest(x) → x itself; the
18966        // FROM-SRF rewrite family → their scalar array calls) and
18967        // the list rides the multi-arg unnest zip channel:
18968        // NULL-padded to the longest, WITH ORDINALITY appends the
18969        // counter. generate_series has no scalar array form and
18970        // errors honestly.
18971        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18972            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18973            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18974        {
18975            self.advance(); // ROWS
18976            self.advance(); // FROM
18977            self.advance(); // (
18978            let mut entries: Vec<Expr> = Vec::new();
18979            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18980            // Used only when some entry has no array form.
18981            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18982            loop {
18983                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18984                if !matches!(self.peek(), Token::LParen) {
18985                    return Err(self.err(alloc::format!(
18986                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
18987                        self.peek()
18988                    )));
18989                }
18990                self.advance();
18991                let mut fn_args: Vec<Expr> = Vec::new();
18992                if !matches!(self.peek(), Token::RParen) {
18993                    loop {
18994                        fn_args.push(self.parse_expr(0)?);
18995                        if matches!(self.peek(), Token::Comma) {
18996                            self.advance();
18997                            continue;
18998                        }
18999                        break;
19000                    }
19001                }
19002                if !matches!(self.peek(), Token::RParen) {
19003                    return Err(self.err(alloc::format!(
19004                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19005                        self.peek()
19006                    )));
19007                }
19008                self.advance();
19009                let entry = match fn_name.as_str() {
19010                    "unnest" => {
19011                        if fn_args.len() != 1 {
19012                            return Err(
19013                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19014                            );
19015                        }
19016                        fn_args.pop().expect("len checked")
19017                    }
19018                    "jsonb_array_elements"
19019                    | "json_array_elements"
19020                    | "jsonb_array_elements_text"
19021                    | "json_array_elements_text"
19022                    | "jsonb_object_keys"
19023                    | "json_object_keys"
19024                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19025                        name: fn_name,
19026                        args: fn_args,
19027                    },
19028                    "string_to_table" => crate::ast::Expr::FunctionCall {
19029                        name: "string_to_array".to_string(),
19030                        args: fn_args,
19031                    },
19032                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19033                        name: "regexp_split_to_array".to_string(),
19034                        args: fn_args,
19035                    },
19036                    // v7.39 (read01 round 74) — an SRF with no array form
19037                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19038                    // scalar expression to zip, so the WHOLE list switches to the
19039                    // rows_from channel, which runs each function and zips the
19040                    // rows themselves. The all-array case keeps the old lowering:
19041                    // it is well-trodden and this must not disturb it.
19042                    _ => {
19043                        generic.push((fn_name, fn_args));
19044                        if matches!(self.peek(), Token::Comma) {
19045                            self.advance();
19046                            continue;
19047                        }
19048                        break;
19049                    }
19050                };
19051                generic.push((
19052                    // The array-able entries carry their lowered expr along, so a
19053                    // MIXED list still works: the engine sees the scalar array
19054                    // form and unnests it.
19055                    "__array".to_string(),
19056                    alloc::vec![entry.clone()],
19057                ));
19058                entries.push(entry);
19059                if matches!(self.peek(), Token::Comma) {
19060                    self.advance();
19061                    continue;
19062                }
19063                break;
19064            }
19065            if !matches!(self.peek(), Token::RParen) {
19066                return Err(self.err(alloc::format!(
19067                    "expected ')' to close ROWS FROM, got {:?}",
19068                    self.peek()
19069                )));
19070            }
19071            self.advance();
19072            let with_ordinality = self.absorb_with_ordinality();
19073            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19074            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19075            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19076            // list rides the generic channel.
19077            if generic.iter().any(|(n, _)| n != "__array") {
19078                let correlated = generic
19079                    .iter()
19080                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19081                let tref = TableRef {
19082                    name,
19083                    alias: alias_ident,
19084                    only: false,
19085                    as_of_segment: None,
19086                    unnest_expr: None,
19087                    unnest_column_aliases,
19088                    with_ordinality,
19089                    generate_series_args: None,
19090                    lateral_subquery: None,
19091                    jsonb_each_text_arg: None,
19092                    table_fn_call: None,
19093                    rows_from: Some(generic),
19094                    json_table: None,
19095                    scalar_fn_item: false,
19096                };
19097                return Ok(if correlated {
19098                    Self::wrap_correlated_srf(tref)
19099                } else {
19100                    tref
19101                });
19102            }
19103            let correlated = entries.iter().any(Self::expr_has_any_column);
19104            let expr = if entries.len() == 1 {
19105                entries.pop().expect("len checked")
19106            } else {
19107                crate::ast::Expr::FunctionCall {
19108                    name: "__unnest_zip".to_string(),
19109                    args: entries,
19110                }
19111            };
19112            let tref = TableRef {
19113                name,
19114                alias: alias_ident,
19115                only: false,
19116                as_of_segment: None,
19117                unnest_expr: Some(Box::new(expr)),
19118                unnest_column_aliases,
19119                with_ordinality,
19120                generate_series_args: None,
19121                lateral_subquery: None,
19122                jsonb_each_text_arg: None,
19123                table_fn_call: None,
19124                rows_from: None,
19125                json_table: None,
19126                scalar_fn_item: false,
19127            };
19128            return Ok(if correlated {
19129                Self::wrap_correlated_srf(tref)
19130            } else {
19131                tref
19132            });
19133        }
19134        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19135        // source. Detect at the head before the bare-ident fallback;
19136        // unnest is not a reserved token.
19137        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19138            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19139        {
19140            self.advance(); // unnest
19141            self.advance(); // (
19142            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19143            while matches!(self.peek(), Token::Comma) {
19144                self.advance();
19145                srf_args.push(self.parse_expr(0)?);
19146            }
19147            if !matches!(self.peek(), Token::RParen) {
19148                return Err(self.err(alloc::format!(
19149                    "expected ')' after unnest() argument, got {:?}",
19150                    self.peek()
19151                )));
19152            }
19153            self.advance();
19154            // Multi-arg unnest(a, b, …) zips the arrays in
19155            // parallel, NULL-padding to the longest (PG's ROWS
19156            // FROM shorthand). Lower onto the unnest channel as an
19157            // internal marker call the executors unpack.
19158            let expr = if srf_args.len() == 1 {
19159                srf_args.pop().expect("len checked")
19160            } else {
19161                crate::ast::Expr::FunctionCall {
19162                    name: "__unnest_zip".to_string(),
19163                    args: srf_args,
19164                }
19165            };
19166            let with_ordinality = self.absorb_with_ordinality();
19167            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19168            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19169            let correlated = Self::expr_has_any_column(&expr);
19170            let tref = TableRef {
19171                name,
19172                alias: alias_ident,
19173                only: false,
19174                as_of_segment: None,
19175                unnest_expr: Some(Box::new(expr)),
19176                unnest_column_aliases,
19177                with_ordinality,
19178                generate_series_args: None,
19179                lateral_subquery: None,
19180                jsonb_each_text_arg: None,
19181                table_fn_call: None,
19182                rows_from: None,
19183                json_table: None,
19184                scalar_fn_item: false,
19185            };
19186            return Ok(if correlated {
19187                Self::wrap_correlated_srf(tref)
19188            } else {
19189                tref
19190            });
19191        }
19192        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19193        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19194        // generic table-fn arg parser can't read), so it is intercepted
19195        // here BEFORE the generic dispatch. The doc expr may reference
19196        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19197        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19198                if s.eq_ignore_ascii_case("json_table"))
19199            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19200        {
19201            let tref = self.parse_json_table_ref()?;
19202            let correlated = tref
19203                .json_table
19204                .as_deref()
19205                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19206            return Ok(if correlated {
19207                Self::wrap_correlated_srf(tref)
19208            } else {
19209                tref
19210            });
19211        }
19212        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19213        // functions dispatched by name (`pg_partition_tree('t')`,
19214        // `pg_partition_ancestors('t')`). Same head-detection shape as
19215        // unnest; the engine executor owns the row shape per function.
19216        // v7.39 (read01 round 65) — and a USER function in FROM position
19217        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19218        // (generate_series / unnest / the json_each family) keep it — their arms
19219        // sit further down, so they are excluded here by name rather than by
19220        // ordering. Anything else that is an ident followed by `(` is a table
19221        // function; the engine executor decides whether it is a builtin, a
19222        // set-returning user function, or an error.
19223        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19224                if !s.eq_ignore_ascii_case("generate_series")
19225                    && !s.eq_ignore_ascii_case("unnest")
19226                    && !is_json_each_name(s))
19227            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19228        {
19229            // Body out-of-line — this parse sits on the FROM/subquery
19230            // recursion chain (debug frame-cliff discipline).
19231            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19232            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19233            // outer row, so it rides the lateral channel. Same rule the unnest
19234            // arm uses.
19235            let tref = self.parse_table_fn_ref()?;
19236            let correlated = tref
19237                .table_fn_call
19238                .as_deref()
19239                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19240            return Ok(if correlated {
19241                Self::wrap_correlated_srf(tref)
19242            } else {
19243                tref
19244            });
19245        }
19246        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19247        // [, step])` set-returning source. Same shape as unnest:
19248        // detect at the head, parse the comma-separated arg list,
19249        // dispatch downstream through the engine's set-returning
19250        // path. Supports integer triplets (mailrs's `WITH row_no AS
19251        // (SELECT * FROM generate_series(1, N))` pattern) and
19252        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19253        // date-range iteration pattern, which pre-3.10 had no
19254        // direct equivalent in SPG).
19255        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19256            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19257        {
19258            self.advance(); // generate_series
19259            self.advance(); // (
19260            let mut args: Vec<Expr> = Vec::new();
19261            loop {
19262                args.push(self.parse_expr(0)?);
19263                if matches!(self.peek(), Token::Comma) {
19264                    self.advance();
19265                    continue;
19266                }
19267                break;
19268            }
19269            if !matches!(self.peek(), Token::RParen) {
19270                return Err(self.err(alloc::format!(
19271                    "expected ')' after generate_series() arguments, got {:?}",
19272                    self.peek()
19273                )));
19274            }
19275            self.advance();
19276            if args.len() < 2 || args.len() > 3 {
19277                return Err(self.err(alloc::format!(
19278                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19279                    args.len()
19280                )));
19281            }
19282            let with_ordinality = self.absorb_with_ordinality();
19283            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19284            let name = alias_ident
19285                .clone()
19286                .unwrap_or_else(|| "generate_series".to_string());
19287            let correlated = args.iter().any(Self::expr_has_any_column);
19288            let tref = TableRef {
19289                name,
19290                alias: alias_ident,
19291                only: false,
19292                as_of_segment: None,
19293                unnest_expr: None,
19294                unnest_column_aliases: column_aliases,
19295                with_ordinality,
19296                generate_series_args: Some(args),
19297                lateral_subquery: None,
19298                jsonb_each_text_arg: None,
19299                table_fn_call: None,
19300                rows_from: None,
19301                json_table: None,
19302                scalar_fn_item: false,
19303            };
19304            return Ok(if correlated {
19305                Self::wrap_correlated_srf(tref)
19306            } else {
19307                tref
19308            });
19309        }
19310        // v7.16.2 — preserve information_schema / pg_catalog
19311        // qualifiers (mailrs round-10 A.3). The generic
19312        // `expect_ident_like` strip silently drops the schema;
19313        // we want the engine to recognise these PG meta tables
19314        // and synthesise rows from the live catalog. Produce a
19315        // synthetic name (`__spg_info_columns` etc.) so the
19316        // engine's SELECT-side router can dispatch without
19317        // clashing with any user-defined `columns` table.
19318        let name = if let Some(synth) = self.try_peek_meta_qualified() {
19319            synth
19320        } else if let Some(synth) = self.try_peek_meta_bare() {
19321            synth
19322        } else {
19323            self.expect_ident_like()?
19324        };
19325        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19326        // time-travel clause. Parse BEFORE the alias so the
19327        // alias can still ride at the tail (`tbl AS OF SEGMENT
19328        // '5' alias`). `AS` is a reserved keyword token, while
19329        // `OF` and `SEGMENT` are bare idents.
19330        let as_of_segment = if matches!(self.peek(), Token::As)
19331            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19332        {
19333            self.advance(); // AS
19334            self.advance(); // OF
19335            let kw = match self.peek().clone() {
19336                Token::Ident(s) | Token::QuotedIdent(s) => s,
19337                other => {
19338                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19339                }
19340            };
19341            if !kw.eq_ignore_ascii_case("segment") {
19342                return Err(self.err(format!(
19343                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19344                )));
19345            }
19346            self.advance();
19347            // Segment id literal — accept either a string or
19348            // integer for operator ergonomics.
19349            let id = match self.advance() {
19350                Token::String(s) => s
19351                    .parse::<u32>()
19352                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19353                Token::Integer(n) => u32::try_from(n)
19354                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19355                other => {
19356                    return Err(self.err(format!(
19357                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19358                    )));
19359                }
19360            };
19361            Some(id)
19362        } else {
19363            None
19364        };
19365        // TABLESAMPLE is not a reserved token — keep the bare-ident
19366        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19367        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19368        {
19369            None
19370        } else {
19371            self.parse_optional_alias()?
19372        };
19373        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19374        // (PG grammar). BERNOULLI lowers to a per-row
19375        // `random() < p/100` conjunct on the enclosing SELECT's
19376        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19377        // shares the lowering: SPG has no page structure to
19378        // sample, and the row-level form returns the same expected
19379        // fraction. REPEATABLE(seed) promises a deterministic
19380        // sample SPG cannot honour yet — honest error rather than
19381        // a silently ignored seed.
19382        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19383            self.advance();
19384            let method = self.expect_ident_like()?;
19385            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19386                return Err(self.err(alloc::format!(
19387                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19388                )));
19389            }
19390            if !matches!(self.peek(), Token::LParen) {
19391                return Err(self.err(alloc::format!(
19392                    "expected '(' after TABLESAMPLE {}, got {:?}",
19393                    method.to_ascii_uppercase(),
19394                    self.peek()
19395                )));
19396            }
19397            self.advance();
19398            let percent = self.parse_expr(0)?;
19399            if !matches!(self.peek(), Token::RParen) {
19400                return Err(self.err(alloc::format!(
19401                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19402                    self.peek()
19403                )));
19404            }
19405            self.advance();
19406            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19407            // `seed`, so the sample is stable across repeats and rescans.
19408            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19409            let mut sample_seed: Option<Expr> = None;
19410            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19411                self.advance();
19412                if !matches!(self.peek(), Token::LParen) {
19413                    return Err(self.err(alloc::format!(
19414                        "expected '(' after REPEATABLE, got {:?}",
19415                        self.peek()
19416                    )));
19417                }
19418                self.advance();
19419                let seed = self.parse_expr(0)?;
19420                if !matches!(self.peek(), Token::RParen) {
19421                    return Err(self.err(alloc::format!(
19422                        "expected ')' after REPEATABLE seed, got {:?}",
19423                        self.peek()
19424                    )));
19425                }
19426                self.advance();
19427                sample_seed = Some(seed);
19428            }
19429            let draw = match sample_seed {
19430                Some(seed) => Expr::FunctionCall {
19431                    name: "__tsm_fract".to_string(),
19432                    args: alloc::vec![seed],
19433                },
19434                None => Expr::FunctionCall {
19435                    name: "random".to_string(),
19436                    args: Vec::new(),
19437                },
19438            };
19439            self.pending_sample_preds.push(Expr::Binary {
19440                lhs: Box::new(draw),
19441                op: crate::ast::BinOp::Lt,
19442                rhs: Box::new(Expr::Binary {
19443                    lhs: Box::new(percent),
19444                    op: crate::ast::BinOp::Div,
19445                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19446                }),
19447            });
19448        }
19449        Ok(TableRef {
19450            name,
19451            alias,
19452            only,
19453            as_of_segment,
19454            unnest_expr: None,
19455            unnest_column_aliases: Vec::new(),
19456            with_ordinality: false,
19457            generate_series_args: None,
19458            lateral_subquery: None,
19459            jsonb_each_text_arg: None,
19460            table_fn_call: None,
19461            rows_from: None,
19462            json_table: None,
19463            scalar_fn_item: false,
19464        })
19465    }
19466
19467    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19468    /// but also accepts `AS alias(col [, col, …])` — the
19469    /// PG-standard table-function column-list form. The column
19470    /// list is only honoured when paired with `UNNEST(...)` in
19471    /// the parent; other call sites currently discard it.
19472    /// True when the expression tree contains a qualified column
19473    /// reference (`t.col`) — the syntactic marker that an SRF
19474    /// argument correlates with a preceding FROM item.
19475    fn expr_has_qualified_column(e: &Expr) -> bool {
19476        match e {
19477            Expr::Column(c) => c.qualifier.is_some(),
19478            Expr::Binary { lhs, rhs, .. } => {
19479                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19480            }
19481            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19482            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19483            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19484            Expr::Case {
19485                operand,
19486                branches,
19487                else_branch,
19488            } => {
19489                operand
19490                    .as_deref()
19491                    .is_some_and(Self::expr_has_qualified_column)
19492                    || branches.iter().any(|(w, t)| {
19493                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19494                    })
19495                    || else_branch
19496                        .as_deref()
19497                        .is_some_and(Self::expr_has_qualified_column)
19498            }
19499            _ => false,
19500        }
19501    }
19502
19503    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19504    /// counts a bare (unqualified) column. A set-returning function has no
19505    /// input columns of its own, so ANY column in its arguments is an outer
19506    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19507    fn expr_has_any_column(e: &Expr) -> bool {
19508        match e {
19509            Expr::Column(_) => true,
19510            Expr::Binary { lhs, rhs, .. } => {
19511                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19512            }
19513            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19514            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19515            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19516            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19517            // constructor or subscript fell to the `_ => false` arm, so
19518            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19519            // channel and the eager peer eval answered `column "x" does
19520            // not exist` (the substitution walker already recurses both
19521            // shapes; only this detector was blind to them).
19522            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19523            Expr::ArraySubscript { target, index } => {
19524                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19525            }
19526            Expr::Case {
19527                operand,
19528                branches,
19529                else_branch,
19530            } => {
19531                operand.as_deref().is_some_and(Self::expr_has_any_column)
19532                    || branches
19533                        .iter()
19534                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19535                    || else_branch
19536                        .as_deref()
19537                        .is_some_and(Self::expr_has_any_column)
19538            }
19539            _ => false,
19540        }
19541    }
19542
19543    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19544    /// `generate_series(1, t.n)`) into the lateral_subquery
19545    /// channel: `SELECT * FROM <srf>` executes per outer row with
19546    /// outer references substituted (v7.37.43-T4.5 machinery).
19547    /// Uncorrelated SRFs stay on their plain channels.
19548    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19549        let name = srf.name.clone();
19550        let alias = srf.alias.clone();
19551        let inner = crate::ast::SelectStatement {
19552            locking: None,
19553            ctes: Vec::new(),
19554            distinct: false,
19555            distinct_on: Vec::new(),
19556            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19557            from: Some(crate::ast::FromClause {
19558                primary: srf,
19559                joins: Vec::new(),
19560            }),
19561            where_: None,
19562            group_by: None,
19563            group_by_all: false,
19564            having: None,
19565            unions: Vec::new(),
19566            order_by: Vec::new(),
19567            limit: None,
19568            offset: None,
19569            limit_with_ties: false,
19570            window_check_exprs: Vec::new(),
19571        };
19572        TableRef {
19573            name,
19574            alias,
19575            only: false,
19576            as_of_segment: None,
19577            unnest_expr: None,
19578            unnest_column_aliases: Vec::new(),
19579            with_ordinality: false,
19580            generate_series_args: None,
19581            lateral_subquery: Some(Box::new(inner)),
19582            jsonb_each_text_arg: None,
19583            table_fn_call: None,
19584            rows_from: None,
19585            json_table: None,
19586            scalar_fn_item: false,
19587        }
19588    }
19589
19590    /// True when the expression tree contains an unresolved
19591    /// `OVER w` marker (see parse_over_clause).
19592    fn expr_has_named_window(e: &Expr) -> bool {
19593        match e {
19594            Expr::WindowFunction { partition_by, .. } => matches!(
19595                partition_by.as_slice(),
19596                [Expr::Column(c)] if matches!(
19597                    c.qualifier.as_deref(),
19598                    Some("__named_window__") | Some("__named_window_ref__")
19599                )
19600            ),
19601            Expr::Binary { lhs, rhs, .. } => {
19602                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19603            }
19604            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19605            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19606            Expr::Case {
19607                operand,
19608                branches,
19609                else_branch,
19610            } => {
19611                operand.as_deref().is_some_and(Self::expr_has_named_window)
19612                    || branches.iter().any(|(w, t)| {
19613                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19614                    })
19615                    || else_branch
19616                        .as_deref()
19617                        .is_some_and(Self::expr_has_named_window)
19618            }
19619            _ => false,
19620        }
19621    }
19622
19623    /// v7.39 (round 705) — the NAMES the expression references through the
19624    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19625    /// definitions nothing referenced. Traversal mirrors
19626    /// `expr_has_named_window` above.
19627    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19628        match e {
19629            Expr::WindowFunction { partition_by, .. } => {
19630                if let [Expr::Column(c)] = partition_by.as_slice()
19631                    && matches!(
19632                        c.qualifier.as_deref(),
19633                        Some("__named_window__") | Some("__named_window_ref__")
19634                    )
19635                {
19636                    into.push(c.name.clone());
19637                }
19638            }
19639            Expr::Binary { lhs, rhs, .. } => {
19640                Self::collect_named_window_refs(lhs, into);
19641                Self::collect_named_window_refs(rhs, into);
19642            }
19643            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19644                Self::collect_named_window_refs(expr, into);
19645            }
19646            Expr::FunctionCall { args, .. } => {
19647                for a in args {
19648                    Self::collect_named_window_refs(a, into);
19649                }
19650            }
19651            Expr::Case {
19652                operand,
19653                branches,
19654                else_branch,
19655            } => {
19656                if let Some(o) = operand.as_deref() {
19657                    Self::collect_named_window_refs(o, into);
19658                }
19659                for (w, t) in branches {
19660                    Self::collect_named_window_refs(w, into);
19661                    Self::collect_named_window_refs(t, into);
19662                }
19663                if let Some(eb) = else_branch.as_deref() {
19664                    Self::collect_named_window_refs(eb, into);
19665                }
19666            }
19667            _ => {}
19668        }
19669    }
19670
19671    /// Inline named-window definitions into the `OVER w` markers.
19672    /// An unknown name errors (PG: window "w" does not exist).
19673    #[allow(clippy::type_complexity)]
19674    fn substitute_named_windows(
19675        e: &mut Expr,
19676        defs: &[(
19677            String,
19678            (
19679                Vec<Expr>,
19680                Vec<(Expr, bool, Option<bool>)>,
19681                Option<WindowFrame>,
19682            ),
19683        )],
19684    ) -> Result<(), String> {
19685        match e {
19686            Expr::WindowFunction {
19687                partition_by,
19688                order_by,
19689                frame,
19690                ..
19691            } => {
19692                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19693                // from the bare `OVER w1` (a plain reference).
19694                let named = match partition_by.as_slice() {
19695                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19696                        Some("__named_window__") => Some((c.name.clone(), false)),
19697                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19698                        _ => None,
19699                    },
19700                    _ => None,
19701                };
19702                if let Some((wname, is_copy)) = named {
19703                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19704                    else {
19705                        return Err(alloc::format!("window {wname:?} does not exist"));
19706                    };
19707                    if !is_copy {
19708                        *partition_by = def.0.clone();
19709                        *order_by = def.1.clone();
19710                        *frame = def.2.clone();
19711                        return Ok(());
19712                    }
19713                    // v7.39 (round 229) — PG's copy rules, probed against
19714                    // 18.4: a copy inherits the partitioning, may supply an
19715                    // ordering only when the base has none, and may not copy
19716                    // a base that already carries a frame (its own frame
19717                    // would be ambiguous with the inherited one).
19718                    if !def.1.is_empty() && !order_by.is_empty() {
19719                        return Err(alloc::format!(
19720                            "cannot override ORDER BY clause of window \"{wname}\""
19721                        ));
19722                    }
19723                    if def.2.is_some() {
19724                        return Err(alloc::format!(
19725                            "cannot copy window \"{wname}\" because it has a frame clause"
19726                        ));
19727                    }
19728                    *partition_by = def.0.clone();
19729                    if order_by.is_empty() {
19730                        *order_by = def.1.clone();
19731                    }
19732                }
19733                Ok(())
19734            }
19735            Expr::Binary { lhs, rhs, .. } => {
19736                Self::substitute_named_windows(lhs, defs)?;
19737                Self::substitute_named_windows(rhs, defs)
19738            }
19739            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19740                Self::substitute_named_windows(expr, defs)
19741            }
19742            Expr::FunctionCall { args, .. } => {
19743                for a in args {
19744                    Self::substitute_named_windows(a, defs)?;
19745                }
19746                Ok(())
19747            }
19748            Expr::Case {
19749                operand,
19750                branches,
19751                else_branch,
19752            } => {
19753                if let Some(op) = operand {
19754                    Self::substitute_named_windows(op, defs)?;
19755                }
19756                for (w, t) in branches {
19757                    Self::substitute_named_windows(w, defs)?;
19758                    Self::substitute_named_windows(t, defs)?;
19759                }
19760                if let Some(el) = else_branch {
19761                    Self::substitute_named_windows(el, defs)?;
19762                }
19763                Ok(())
19764            }
19765            _ => Ok(()),
19766        }
19767    }
19768
19769    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19770    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19771    /// composition.
19772    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19773        debug_assert!(matches!(self.peek(), Token::Table));
19774        self.advance(); // TABLE
19775        let tname = self.expect_ident_like()?;
19776        Ok(SelectStatement {
19777            locking: None,
19778            ctes: Vec::new(),
19779            distinct: false,
19780            distinct_on: Vec::new(),
19781            items: alloc::vec![SelectItem::Wildcard],
19782            from: Some(FromClause {
19783                primary: TableRef {
19784                    name: tname,
19785                    alias: None,
19786                    only: false,
19787                    as_of_segment: None,
19788                    unnest_expr: None,
19789                    unnest_column_aliases: Vec::new(),
19790                    with_ordinality: false,
19791                    generate_series_args: None,
19792                    lateral_subquery: None,
19793                    jsonb_each_text_arg: None,
19794                    table_fn_call: None,
19795                    rows_from: None,
19796                    json_table: None,
19797                    scalar_fn_item: false,
19798                },
19799                joins: Vec::new(),
19800            }),
19801            where_: None,
19802            group_by: None,
19803            group_by_all: false,
19804            having: None,
19805            unions: Vec::new(),
19806            order_by: Vec::new(),
19807            limit: None,
19808            offset: None,
19809            limit_with_ties: false,
19810            window_check_exprs: Vec::new(),
19811        })
19812    }
19813
19814    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19815    /// variants) → a derived table that reads each declared column out of
19816    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19817    /// `jsonb_array_elements(J)` (one row per element, column `value`);
19818    /// the scalar *record form projects a single row straight off `J`.
19819    /// Rides the existing lateral-subquery channel, so no new executor or
19820    /// AST is needed.
19821    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19822        use crate::ast::{
19823            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19824        };
19825        let fn_name = match self.peek() {
19826            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19827            _ => unreachable!("caller guarded is_json_to_record_name"),
19828        };
19829        self.advance(); // fn name
19830        self.advance(); // (
19831        let mut arg = self.parse_expr(0)?;
19832        // populate_record(base, json): the base only carries the record
19833        // type here — the JSON argument is the second expression.
19834        let mut base: Option<Expr> = None;
19835        if matches!(self.peek(), Token::Comma) {
19836            self.advance();
19837            base = Some(arg);
19838            arg = self.parse_expr(0)?;
19839        }
19840        if !matches!(self.peek(), Token::RParen) {
19841            return Err(self.err(alloc::format!(
19842                "expected ')' after {fn_name}() argument, got {:?}",
19843                self.peek()
19844            )));
19845        }
19846        self.advance(); // )
19847        let is_set = fn_name.ends_with("recordset");
19848        // `[AS] alias ( col type [, …] )` column-definition list.
19849        if matches!(self.peek(), Token::As) {
19850            self.advance();
19851        }
19852        let alias_opt = match self.peek() {
19853            Token::Ident(s) | Token::QuotedIdent(s) => {
19854                let a = s.clone();
19855                self.advance();
19856                Some(a)
19857            }
19858            _ => None,
19859        };
19860        // v7.39 (read01 round 76) — the populate family's canonical PG
19861        // spelling carries no column list at all: the row shape comes from
19862        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19863        // j)`). The parser has no catalog, so hand the two arguments to the
19864        // engine's table-function channel, which does. Only `*_to_record*`
19865        // (whose base is bare `record`) genuinely requires the list.
19866        if !matches!(self.peek(), Token::LParen) {
19867            if let Some(base_expr) = base {
19868                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19869                return Ok(TableRef {
19870                    name: alias.clone(),
19871                    alias: Some(alias),
19872                    only: false,
19873                    as_of_segment: None,
19874                    unnest_expr: None,
19875                    unnest_column_aliases: Vec::new(),
19876                    with_ordinality: false,
19877                    generate_series_args: None,
19878                    lateral_subquery: None,
19879                    jsonb_each_text_arg: None,
19880                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19881                    rows_from: None,
19882                    json_table: None,
19883                    scalar_fn_item: false,
19884                });
19885            }
19886            return Err(self.err(alloc::format!(
19887                "expected '(' to start the {fn_name} column-definition list, got {:?}",
19888                self.peek()
19889            )));
19890        }
19891        let Some(alias) = alias_opt else {
19892            return Err(self.err(alloc::format!(
19893                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19894            )));
19895        };
19896        self.advance(); // (
19897        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19898        loop {
19899            let col = self.expect_ident_like()?;
19900            let ty = self.parse_cast_target()?;
19901            coldefs.push((col, ty));
19902            if matches!(self.peek(), Token::Comma) {
19903                self.advance();
19904                continue;
19905            }
19906            if matches!(self.peek(), Token::RParen) {
19907                self.advance();
19908                break;
19909            }
19910            return Err(self.err(alloc::format!(
19911                "expected ',' or ')' in {fn_name} column list, got {:?}",
19912                self.peek()
19913            )));
19914        }
19915        if coldefs.is_empty() {
19916            return Err(self.err(alloc::format!(
19917                "{fn_name} column-definition list must declare at least one column"
19918            )));
19919        }
19920        // Per column: (base ->> 'col')::type AS col. The base is the
19921        // per-element `value` column for the *set form, or the argument
19922        // itself for the scalar record form.
19923        let items: Vec<SelectItem> = coldefs
19924            .into_iter()
19925            .map(|(col, ty)| {
19926                let base = if is_set {
19927                    Expr::Column(ColumnName {
19928                        qualifier: None,
19929                        name: "value".to_string(),
19930                    })
19931                } else {
19932                    arg.clone()
19933                };
19934                SelectItem::Expr {
19935                    expr: Expr::Cast {
19936                        expr: Box::new(Expr::Binary {
19937                            lhs: Box::new(base),
19938                            op: BinOp::JsonGetText,
19939                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19940                        }),
19941                        target: ty,
19942                    },
19943                    alias: Some(col),
19944                }
19945            })
19946            .collect();
19947        let from = if is_set {
19948            let elem_fn = if fn_name.starts_with("jsonb") {
19949                "jsonb_array_elements"
19950            } else {
19951                "json_array_elements"
19952            };
19953            Some(FromClause {
19954                primary: TableRef {
19955                    name: "value".to_string(),
19956                    alias: None,
19957                    only: false,
19958                    as_of_segment: None,
19959                    unnest_expr: Some(Box::new(Expr::FunctionCall {
19960                        name: elem_fn.to_string(),
19961                        args: alloc::vec![arg],
19962                    })),
19963                    unnest_column_aliases: alloc::vec!["value".to_string()],
19964                    with_ordinality: false,
19965                    generate_series_args: None,
19966                    lateral_subquery: None,
19967                    jsonb_each_text_arg: None,
19968                    table_fn_call: None,
19969                    rows_from: None,
19970                    json_table: None,
19971                    scalar_fn_item: false,
19972                },
19973                joins: Vec::new(),
19974            })
19975        } else {
19976            None
19977        };
19978        let inner = SelectStatement {
19979            locking: None,
19980            ctes: Vec::new(),
19981            distinct: false,
19982            distinct_on: Vec::new(),
19983            items,
19984            from,
19985            where_: None,
19986            group_by: None,
19987            group_by_all: false,
19988            having: None,
19989            unions: Vec::new(),
19990            order_by: Vec::new(),
19991            limit: None,
19992            offset: None,
19993            limit_with_ties: false,
19994            window_check_exprs: Vec::new(),
19995        };
19996        Ok(TableRef {
19997            name: alias.clone(),
19998            alias: Some(alias),
19999            only: false,
20000            as_of_segment: None,
20001            unnest_expr: None,
20002            unnest_column_aliases: Vec::new(),
20003            with_ordinality: false,
20004            generate_series_args: None,
20005            lateral_subquery: Some(Box::new(inner)),
20006            jsonb_each_text_arg: None,
20007            table_fn_call: None,
20008            rows_from: None,
20009            json_table: None,
20010            scalar_fn_item: false,
20011        })
20012    }
20013
20014    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20015    /// Returns true when the clause was present. `WITH` alone (a
20016    /// CTE can never start here) is not enough — the ORDINALITY
20017    /// ident must follow, so a stray WITH still errors downstream.
20018    fn absorb_with_ordinality(&mut self) -> bool {
20019        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20020            && matches!(self.tokens.get(self.pos + 1),
20021                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20022        {
20023            self.advance();
20024            self.advance();
20025            true
20026        } else {
20027            false
20028        }
20029    }
20030
20031    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20032    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20033    /// Out-of-line: the caller sits on the FROM recursion chain.
20034    #[inline(never)]
20035    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20036        let fn_name = match self.advance() {
20037            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20038            _ => unreachable!("caller peeked an ident"),
20039        };
20040        self.advance(); // (
20041        let mut args: Vec<Expr> = Vec::new();
20042        if !matches!(self.peek(), Token::RParen) {
20043            loop {
20044                args.push(self.parse_expr(0)?);
20045                if matches!(self.peek(), Token::Comma) {
20046                    self.advance();
20047                    continue;
20048                }
20049                break;
20050            }
20051        }
20052        if !matches!(self.peek(), Token::RParen) {
20053            return Err(self.err(alloc::format!(
20054                "expected ')' after {fn_name}() arguments, got {:?}",
20055                self.peek()
20056            )));
20057        }
20058        self.advance();
20059        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20060        // counter column rides after the function's own, and the alias list
20061        // names it.
20062        let with_ordinality = self.absorb_with_ordinality();
20063        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20064        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20065        Ok(TableRef {
20066            name,
20067            alias: alias_ident,
20068            only: false,
20069            as_of_segment: None,
20070            unnest_expr: None,
20071            unnest_column_aliases,
20072            with_ordinality,
20073            generate_series_args: None,
20074            lateral_subquery: None,
20075            jsonb_each_text_arg: None,
20076            table_fn_call: Some(Box::new((fn_name, args))),
20077            rows_from: None,
20078            json_table: None,
20079            scalar_fn_item: false,
20080        })
20081    }
20082
20083    /// v7.39 (round 205, JSON_TABLE) — parse
20084    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20085    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20086    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20087    #[inline(never)]
20088    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20089        self.advance(); // json_table
20090        self.advance(); // (
20091        let doc = Box::new(self.parse_expr(0)?);
20092        self.expect_comma_json_table()?;
20093        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20094        // Optional `PASSING <expr> AS <name> [, …]`.
20095        let mut passing: Vec<(String, Expr)> = Vec::new();
20096        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20097            self.advance();
20098            loop {
20099                let e = self.parse_expr(0)?;
20100                if !matches!(self.peek(), Token::As) {
20101                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20102                }
20103                self.advance();
20104                let vname = match self.advance() {
20105                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20106                    other => {
20107                        return Err(self.err(alloc::format!(
20108                            "expected PASSING variable name, got {other:?}"
20109                        )));
20110                    }
20111                };
20112                passing.push((vname, e));
20113                if matches!(self.peek(), Token::Comma) {
20114                    self.advance();
20115                    continue;
20116                }
20117                break;
20118            }
20119        }
20120        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20121            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20122        }
20123        self.advance();
20124        let columns = self.parse_json_table_columns()?;
20125        if !matches!(self.peek(), Token::RParen) {
20126            return Err(self.err(alloc::format!(
20127                "expected ')' to close JSON_TABLE, got {:?}",
20128                self.peek()
20129            )));
20130        }
20131        self.advance();
20132        let alias_ident = self.parse_optional_alias()?;
20133        let name = alias_ident
20134            .clone()
20135            .unwrap_or_else(|| String::from("json_table"));
20136        Ok(TableRef {
20137            name,
20138            alias: alias_ident,
20139            only: false,
20140            as_of_segment: None,
20141            unnest_expr: None,
20142            unnest_column_aliases: Vec::new(),
20143            with_ordinality: false,
20144            generate_series_args: None,
20145            lateral_subquery: None,
20146            jsonb_each_text_arg: None,
20147            table_fn_call: None,
20148            rows_from: None,
20149            json_table: Some(Box::new(crate::ast::JsonTable {
20150                doc,
20151                row_path,
20152                columns,
20153                passing,
20154            })),
20155            scalar_fn_item: false,
20156        })
20157    }
20158
20159    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20160        if !matches!(self.peek(), Token::Comma) {
20161            return Err(self.err(alloc::format!(
20162                "expected ',' after JSON_TABLE document, got {:?}",
20163                self.peek()
20164            )));
20165        }
20166        self.advance();
20167        Ok(())
20168    }
20169
20170    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20171        match self.advance() {
20172            Token::String(s) => Ok(s),
20173            other => Err(self.err(alloc::format!(
20174                "expected {what} string literal, got {other:?}"
20175            ))),
20176        }
20177    }
20178
20179    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20180    #[inline(never)]
20181    fn parse_json_table_columns(
20182        &mut self,
20183    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20184        if !matches!(self.peek(), Token::LParen) {
20185            return Err(self.err("expected '(' after COLUMNS".into()));
20186        }
20187        self.advance();
20188        let mut cols = Vec::new();
20189        loop {
20190            cols.push(self.parse_json_table_one_column()?);
20191            if matches!(self.peek(), Token::Comma) {
20192                self.advance();
20193                continue;
20194            }
20195            break;
20196        }
20197        if !matches!(self.peek(), Token::RParen) {
20198            return Err(self.err(alloc::format!(
20199                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20200                self.peek()
20201            )));
20202        }
20203        self.advance();
20204        Ok(cols)
20205    }
20206
20207    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20208        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20209        // NESTED [PATH] '<p>' COLUMNS (...)
20210        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20211            self.advance();
20212            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20213                self.advance();
20214            }
20215            let path = self.parse_json_string_literal("NESTED PATH")?;
20216            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20217                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20218            }
20219            self.advance();
20220            let columns = self.parse_json_table_columns()?;
20221            return Ok(JsonTableColumn::Nested { path, columns });
20222        }
20223        // <name> ...
20224        let name = match self.advance() {
20225            Token::Ident(s) | Token::QuotedIdent(s) => s,
20226            other => {
20227                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20228            }
20229        };
20230        // <name> FOR ORDINALITY
20231        if matches!(self.peek(), Token::For) {
20232            self.advance();
20233            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20234                return Err(self.err("expected ORDINALITY after FOR".into()));
20235            }
20236            self.advance();
20237            return Ok(JsonTableColumn::Ordinality { name });
20238        }
20239        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20240        let ty = self.parse_column_type_name()?;
20241        let mut format_json = false;
20242        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20243            self.advance();
20244            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20245                return Err(self.err("expected JSON after FORMAT".into()));
20246            }
20247            self.advance();
20248            format_json = true;
20249        }
20250        let mut exists = false;
20251        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20252            self.advance();
20253            exists = true;
20254        }
20255        let mut path = alloc::format!("$.{name}");
20256        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20257            self.advance();
20258            path = self.parse_json_string_literal("column PATH")?;
20259        }
20260        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20261            // `FORMAT JSON` after PATH (alternate placement).
20262            self.advance();
20263            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20264                self.advance();
20265            }
20266            format_json = true;
20267        }
20268        let mut wrapper = false;
20269        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20270            self.advance();
20271            // optional CONDITIONAL/UNCONDITIONAL
20272            if matches!(self.peek(), Token::Ident(s)
20273                if s.eq_ignore_ascii_case("unconditional")
20274                    || s.eq_ignore_ascii_case("conditional"))
20275            {
20276                self.advance();
20277            }
20278            if !matches!(self.peek(), Token::Ident(s)
20279                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20280            {
20281                return Err(self.err("expected WRAPPER after WITH".into()));
20282            }
20283            self.advance();
20284            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20285            if matches!(self.peek(), Token::Ident(s)
20286                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20287            {
20288                self.advance();
20289            }
20290            wrapper = true;
20291        }
20292        // ON EMPTY / ON ERROR clauses (two, in any order).
20293        let mut on_empty = JsonTableOnBehavior::Null;
20294        let mut on_error = JsonTableOnBehavior::Null;
20295        for _ in 0..2 {
20296            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20297            {
20298                self.advance();
20299                Some(JsonTableOnBehavior::Error)
20300            } else if matches!(self.peek(), Token::Null) {
20301                self.advance();
20302                Some(JsonTableOnBehavior::Null)
20303            } else if matches!(self.peek(), Token::Default) {
20304                self.advance();
20305                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20306            } else {
20307                None
20308            };
20309            let Some(behavior) = behavior else { break };
20310            // `ON {EMPTY|ERROR}`
20311            if !matches!(self.peek(), Token::On) {
20312                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20313            }
20314            self.advance();
20315            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20316                self.advance();
20317                on_empty = behavior;
20318            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20319                self.advance();
20320                on_error = behavior;
20321            } else {
20322                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20323            }
20324        }
20325        Ok(JsonTableColumn::Regular {
20326            name,
20327            ty,
20328            path,
20329            exists,
20330            format_json,
20331            wrapper,
20332            on_empty,
20333            on_error,
20334        })
20335    }
20336
20337    fn parse_optional_alias_with_columns(
20338        &mut self,
20339    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20340        let alias = self.parse_optional_alias()?;
20341        if alias.is_none() {
20342            return Ok((None, Vec::new()));
20343        }
20344        let mut cols: Vec<String> = Vec::new();
20345        if matches!(self.peek(), Token::LParen) {
20346            self.advance();
20347            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20348                self.advance();
20349                cols.push(s);
20350                if matches!(self.peek(), Token::Comma) {
20351                    self.advance();
20352                    continue;
20353                }
20354                break;
20355            }
20356            if matches!(self.peek(), Token::RParen) {
20357                self.advance();
20358            }
20359        }
20360        Ok((alias, cols))
20361    }
20362
20363    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20364    /// whose keyword token was already consumed and whose `(` is the
20365    /// current token. Factored out of `parse_atom` (and marked
20366    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20367    /// recursive `parse_atom` frame — inlining them there enlarges the
20368    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20369    /// against, risking an overflow before the budget triggers.
20370    #[inline(never)]
20371    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20372        self.advance(); // (
20373        let mut args = Vec::new();
20374        if !matches!(self.peek(), Token::RParen) {
20375            loop {
20376                args.push(self.parse_expr(0)?);
20377                match self.peek() {
20378                    Token::Comma => {
20379                        self.advance();
20380                    }
20381                    Token::RParen => break,
20382                    other => {
20383                        return Err(self.err(alloc::format!(
20384                            "expected ',' or ')' in {name}() args, got {other:?}"
20385                        )));
20386                    }
20387                }
20388            }
20389        }
20390        self.advance(); // )
20391        Ok(Expr::FunctionCall {
20392            name: name.into(),
20393            args,
20394        })
20395    }
20396
20397    /// FROM-clause: a primary table reference plus zero-or-more joined
20398    /// peers expressed via either `, <table>` (cross-product, no ON) or
20399    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20400    /// v1.10 keeps the join list flat (left-associative nested-loop
20401    /// semantics).
20402    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20403        let primary = self.parse_table_ref()?;
20404        let primary_qual = primary
20405            .alias
20406            .clone()
20407            .unwrap_or_else(|| primary.name.clone());
20408        let joins = self.parse_from_joins(&primary_qual)?;
20409        Ok(FromClause { primary, joins })
20410    }
20411
20412    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20413    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20414    /// SAME grammar after its target table has already been consumed.
20415    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20416    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20417    /// be parsed forward, once.)
20418    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20419    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20420    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20421    /// desugaring, which needs a name for the left side of each equality.
20422    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20423        let mut joins = Vec::new();
20424        loop {
20425            // `, <table>` — cross-product with no ON.
20426            if matches!(self.peek(), Token::Comma) {
20427                self.advance();
20428                let table = self.parse_table_ref()?;
20429                joins.push(FromJoin {
20430                    kind: JoinKind::Cross,
20431                    table,
20432                    on: None,
20433                    using_cols: None,
20434                    natural: false,
20435                });
20436                continue;
20437            }
20438            // v7.37.16 — optional leading `NATURAL` before the join
20439            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20440            // not a lexer keyword (it arrives as a bare Ident), so match
20441            // it case-insensitively here. When present, no ON/USING
20442            // clause is allowed — the common columns are resolved at
20443            // execution time.
20444            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20445            if natural {
20446                self.advance();
20447            }
20448            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20449            // CROSS JOIN, and bare JOIN (defaults to INNER).
20450            let kind =
20451                match self.peek() {
20452                    Token::Inner => {
20453                        self.advance();
20454                        if !matches!(self.peek(), Token::Join) {
20455                            return Err(self
20456                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20457                        }
20458                        self.advance();
20459                        JoinKind::Inner
20460                    }
20461                    Token::Left => {
20462                        self.advance();
20463                        if matches!(self.peek(), Token::Outer) {
20464                            self.advance();
20465                        }
20466                        if !matches!(self.peek(), Token::Join) {
20467                            return Err(self.err(format!(
20468                                "expected JOIN after LEFT [OUTER], got {:?}",
20469                                self.peek()
20470                            )));
20471                        }
20472                        self.advance();
20473                        JoinKind::Left
20474                    }
20475                    Token::Cross => {
20476                        self.advance();
20477                        if !matches!(self.peek(), Token::Join) {
20478                            return Err(self
20479                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20480                        }
20481                        self.advance();
20482                        JoinKind::Cross
20483                    }
20484                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20485                    Token::Right => {
20486                        self.advance();
20487                        if matches!(self.peek(), Token::Outer) {
20488                            self.advance();
20489                        }
20490                        if !matches!(self.peek(), Token::Join) {
20491                            return Err(self.err(format!(
20492                                "expected JOIN after RIGHT [OUTER], got {:?}",
20493                                self.peek()
20494                            )));
20495                        }
20496                        self.advance();
20497                        JoinKind::Right
20498                    }
20499                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20500                    Token::Full => {
20501                        self.advance();
20502                        if matches!(self.peek(), Token::Outer) {
20503                            self.advance();
20504                        }
20505                        if !matches!(self.peek(), Token::Join) {
20506                            return Err(self.err(format!(
20507                                "expected JOIN after FULL [OUTER], got {:?}",
20508                                self.peek()
20509                            )));
20510                        }
20511                        self.advance();
20512                        JoinKind::FullOuter
20513                    }
20514                    Token::Join => {
20515                        self.advance();
20516                        JoinKind::Inner
20517                    }
20518                    _ => break,
20519                };
20520            let table = self.parse_table_ref()?;
20521            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20522            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20523            // where prev_table is the most-recent left-side table
20524            // (the previous join's table if any, else the FROM primary).
20525            // PG semantics around column merging are richer (USING'd
20526            // cols become deduplicated single output columns); for
20527            // sugar purposes the predicate-only form covers the
20528            // baseline corpus shape and chained `… JOIN x USING (k)
20529            // JOIN y USING (k)` calls.
20530            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20531            // common columns resolve at execution time.
20532            if natural {
20533                joins.push(FromJoin {
20534                    kind,
20535                    table,
20536                    on: None,
20537                    using_cols: None,
20538                    natural: true,
20539                });
20540                continue;
20541            }
20542            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20543            // v7.37.16 — capture the USING column list (in addition to
20544            // the ON desugar below) so the executor can perform PG's
20545            // column-merge on the output side.
20546            let mut using_cols: Option<Vec<String>> = None;
20547            let on = if matches!(self.peek(), Token::On) {
20548                self.advance();
20549                Some(self.parse_expr(0)?)
20550            } else if using_match {
20551                self.advance();
20552                if !matches!(self.peek(), Token::LParen) {
20553                    return Err(
20554                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20555                    );
20556                }
20557                self.advance();
20558                let mut cols: Vec<String> = Vec::new();
20559                loop {
20560                    match self.peek().clone() {
20561                        Token::Ident(s) | Token::QuotedIdent(s) => {
20562                            self.advance();
20563                            cols.push(s);
20564                        }
20565                        other => {
20566                            return Err(self.err(format!(
20567                                "expected column name inside USING (…), got {other:?}"
20568                            )));
20569                        }
20570                    }
20571                    match self.peek() {
20572                        Token::Comma => {
20573                            self.advance();
20574                            continue;
20575                        }
20576                        Token::RParen => {
20577                            self.advance();
20578                            break;
20579                        }
20580                        other => {
20581                            return Err(self.err(format!(
20582                                "expected ',' or ')' inside USING (…), got {other:?}"
20583                            )));
20584                        }
20585                    }
20586                }
20587                if cols.is_empty() {
20588                    return Err(self.err("USING (…) requires at least one column".to_string()));
20589                }
20590                using_cols = Some(cols.clone());
20591                // Pick the left-side alias: prev join's table if any,
20592                // else FROM primary. Use alias when present, else
20593                // table name (PG-equivalent qualifier).
20594                let left_qual: String = joins
20595                    .last()
20596                    .map(|j| {
20597                        j.table
20598                            .alias
20599                            .clone()
20600                            .unwrap_or_else(|| j.table.name.clone())
20601                    })
20602                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20603                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20604                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20605                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20606                        qualifier: Some(left_qual.clone()),
20607                        name: c.clone(),
20608                    })),
20609                    op: crate::ast::BinOp::Eq,
20610                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20611                        qualifier: Some(right_qual.clone()),
20612                        name: c,
20613                    })),
20614                });
20615                let first = iter.next().expect("at least one col");
20616                Some(iter.fold(first, |acc, pred| Expr::Binary {
20617                    lhs: alloc::boxed::Box::new(acc),
20618                    op: crate::ast::BinOp::And,
20619                    rhs: alloc::boxed::Box::new(pred),
20620                }))
20621            } else if kind == JoinKind::Cross {
20622                None
20623            } else {
20624                return Err(self.err(format!(
20625                    "expected ON or USING after {:?} JOIN, got {:?}",
20626                    kind,
20627                    self.peek()
20628                )));
20629            };
20630            joins.push(FromJoin {
20631                kind,
20632                table,
20633                on,
20634                using_cols,
20635                natural: false,
20636            });
20637        }
20638        Ok(joins)
20639    }
20640
20641    /// Optional alias after an expression or table:
20642    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20643    /// accepted (PG-style implicit alias). Returns `None` if the next token
20644    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20645    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20646        if matches!(self.peek(), Token::As) {
20647            self.advance();
20648            // v7.39 (round 340, V56) — after AS the next token MUST be an
20649            // identifier. This used to return None and "let the caller
20650            // surface the error on the next expectation", but when AS is
20651            // the LAST token there is no next expectation: `SELECT 1 AS`
20652            // parsed clean and silently dropped the alias. PG rejects it.
20653            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20654                return self.expect_ident_like().map(Some);
20655            }
20656            return Err(self.err(alloc::format!(
20657                "expected an alias after AS, got {:?}",
20658                self.peek()
20659            )));
20660        }
20661        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20662        // grammar reserves a long list of follow-keywords from the
20663        // alias slot. SPG's bareword approximation: skip a small
20664        // set of idents that would otherwise be swallowed as the
20665        // table alias and break trailing clauses like CREATE
20666        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20667        // CONFLICT WHERE shapes.
20668        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20669            if is_alias_stopword(s) {
20670                return Ok(None);
20671            }
20672            return Ok(self.expect_ident_like().ok());
20673        }
20674        Ok(None)
20675    }
20676
20677    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20678    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20679        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20680        // error beats a stack overflow (an overflow aborts the
20681        // embedding host process).
20682        self.enter_nested()?;
20683        let r = self.parse_expr_inner(min_prec);
20684        self.nest_depth -= 1;
20685        r
20686    }
20687
20688    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20689    /// When the upcoming tokens form one, return the underlying
20690    /// operator token and the position just past the closing paren
20691    /// so the binary loop can dispatch on the plain operator.
20692    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20693        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20694            return None;
20695        }
20696        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20697            return None;
20698        }
20699        let mut i = self.pos + 2;
20700        // Optional schema qualifier (pg_catalog.<op> etc.).
20701        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20702            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20703        {
20704            i += 2;
20705        }
20706        let op_tok = self.tokens.get(i)?.clone();
20707        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20708            return None;
20709        }
20710        Some((i + 2, op_tok))
20711    }
20712
20713    /// PG operator symbols that lower onto function calls in
20714    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20715    /// family → regexp_like, comparison rung), `^@` (starts_with,
20716    /// comparison rung), `^` (power, tighter than `*`), `#`
20717    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20718    /// subset of the OR bits so the subtraction never borrows).
20719    fn try_symbol_operator(
20720        &mut self,
20721        lhs: &Expr,
20722        min_prec: u8,
20723    ) -> Result<Option<Expr>, ParseError> {
20724        enum Sym {
20725            Regex { ci: bool, negated: bool },
20726            Like { ci: bool, negated: bool },
20727            StartsWith,
20728            Power,
20729            Xor,
20730            RangeAdjacent,
20731        }
20732        // v7.39 (IS-precedence knife) — the low-precedence postfix
20733        // predicates ride this existing leaf call (zero new frame slots
20734        // on the nesting chain).
20735        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20736            return Ok(Some(e));
20737        }
20738        let (sym, prec): (Sym, u8) = match self.peek() {
20739            Token::Tilde => (
20740                Sym::Regex {
20741                    ci: false,
20742                    negated: false,
20743                },
20744                5,
20745            ),
20746            Token::TildeStar => (
20747                Sym::Regex {
20748                    ci: true,
20749                    negated: false,
20750                },
20751                5,
20752            ),
20753            Token::NotTilde => (
20754                Sym::Regex {
20755                    ci: false,
20756                    negated: true,
20757                },
20758                5,
20759            ),
20760            Token::NotTildeStar => (
20761                Sym::Regex {
20762                    ci: true,
20763                    negated: true,
20764                },
20765                5,
20766            ),
20767            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20768            Token::DoubleTilde => (
20769                Sym::Like {
20770                    ci: false,
20771                    negated: false,
20772                },
20773                5,
20774            ),
20775            Token::DoubleTildeStar => (
20776                Sym::Like {
20777                    ci: true,
20778                    negated: false,
20779                },
20780                5,
20781            ),
20782            Token::NotDoubleTilde => (
20783                Sym::Like {
20784                    ci: false,
20785                    negated: true,
20786                },
20787                5,
20788            ),
20789            Token::NotDoubleTildeStar => (
20790                Sym::Like {
20791                    ci: true,
20792                    negated: true,
20793                },
20794                5,
20795            ),
20796            Token::CaretAt => (Sym::StartsWith, 5),
20797            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20798            // tighter than `* / & |`, which the prec-9 rung preserves —
20799            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20800            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20801            Token::Caret => (Sym::Power, 9),
20802            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20803            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20804            Token::Hash => (Sym::Xor, 6),
20805            Token::Adjacent => (Sym::RangeAdjacent, 5),
20806            _ => return Ok(None),
20807        };
20808        if prec < min_prec {
20809            return Ok(None);
20810        }
20811        self.advance();
20812        let rhs = self.parse_expr(prec + 1)?;
20813        let out = match sym {
20814            Sym::Regex { ci, negated } => {
20815                let mut args = alloc::vec![lhs.clone(), rhs];
20816                if ci {
20817                    args.push(Expr::Literal(Literal::String(String::from("i"))));
20818                }
20819                maybe_not(
20820                    Expr::FunctionCall {
20821                        name: String::from("regexp_like"),
20822                        args,
20823                    },
20824                    negated,
20825                )
20826            }
20827            Sym::Like { ci, negated } => Expr::Like {
20828                expr: alloc::boxed::Box::new(lhs.clone()),
20829                pattern: alloc::boxed::Box::new(rhs),
20830                negated,
20831                case_insensitive: ci,
20832            },
20833            Sym::StartsWith => Expr::FunctionCall {
20834                name: String::from("starts_with"),
20835                args: alloc::vec![lhs.clone(), rhs],
20836            },
20837            Sym::Power => Expr::FunctionCall {
20838                name: String::from("power"),
20839                args: alloc::vec![lhs.clone(), rhs],
20840            },
20841            // `#` bitwise XOR — a real operator now (was desugared to
20842            // `(a|b)-(a&b)`, algebraically identical for integers but
20843            // undefined for bit strings; the direct op handles both).
20844            Sym::Xor => Expr::Binary {
20845                lhs: Box::new(lhs.clone()),
20846                op: BinOp::BitXor,
20847                rhs: Box::new(rhs),
20848            },
20849            // range `-|-` "is adjacent to" — lowered to a catalog function.
20850            Sym::RangeAdjacent => Expr::FunctionCall {
20851                name: String::from("range_adjacent"),
20852                args: alloc::vec![lhs.clone(), rhs],
20853            },
20854        };
20855        Ok(Some(out))
20856    }
20857
20858    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20859    /// predicates, moved out of the tight postfix-cast loop: PG binds
20860    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20861    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20862    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20863    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20864    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20865    /// when nothing at this position belongs to the family. Out-of-line
20866    /// (`inline(never)`): the caller sits on the per-nesting-level frame
20867    /// chain that MAX_NEST_DEPTH is tuned against.
20868    #[inline(never)]
20869    fn parse_postfix_predicate(
20870        &mut self,
20871        lhs: &Expr,
20872        min_prec: u8,
20873    ) -> Result<Option<Expr>, ParseError> {
20874        // Reached through try_symbol_operator (an existing leaf call of
20875        // the binary loop) so NO new stack slots land on the per-nesting
20876        // frame chain; the lhs clones only when a predicate actually
20877        // consumes it.
20878        match self.peek() {
20879            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20880            // comparison family rung 5 (each +1 from the pre-XOR ladder).
20881            Token::Is if min_prec <= 4 => {}
20882            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20883            Token::Not
20884                if min_prec <= 5
20885                    && matches!(
20886                        self.tokens.get(self.pos + 1),
20887                        Some(Token::Between | Token::In | Token::Like)
20888                    ) => {}
20889            Token::Not | Token::Ident(_)
20890                if min_prec <= 5
20891                    && (matches!(self.peek(), Token::Ident(s)
20892                            if s.eq_ignore_ascii_case("ilike")
20893                                || (self.mysql_dialect
20894                                    && (s.eq_ignore_ascii_case("regexp")
20895                                        || s.eq_ignore_ascii_case("rlike")))
20896                                || (s.eq_ignore_ascii_case("similar")
20897                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20898                        || (matches!(self.peek(), Token::Not)
20899                            && matches!(self.tokens.get(self.pos + 1),
20900                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20901                                    || (self.mysql_dialect
20902                                        && (s.eq_ignore_ascii_case("regexp")
20903                                            || s.eq_ignore_ascii_case("rlike")))
20904                                    || s.eq_ignore_ascii_case("similar")))) => {}
20905            _ => return Ok(None),
20906        }
20907        let mut expr = lhs.clone();
20908        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20909        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20910        if min_prec <= 4 {
20911            if matches!(self.peek(), Token::Is) {
20912                self.advance();
20913                let negated = if matches!(self.peek(), Token::Not) {
20914                    self.advance();
20915                    true
20916                } else {
20917                    false
20918                };
20919                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20920                // mailrs pg_dump.
20921                if matches!(self.peek(), Token::Distinct) {
20922                    self.advance();
20923                    if !matches!(self.peek(), Token::From) {
20924                        return Err(self.err(format!(
20925                            "expected FROM after IS{} DISTINCT, got {:?}",
20926                            if negated { " NOT" } else { "" },
20927                            self.peek()
20928                        )));
20929                    }
20930                    self.advance();
20931                    // Right-hand side: parse at the same precedence
20932                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20933                    // groups as `x IS DISTINCT FROM (a + b)`.
20934                    let rhs = self.parse_expr(5)?;
20935                    let op = if negated {
20936                        BinOp::IsNotDistinctFrom
20937                    } else {
20938                        BinOp::IsDistinctFrom
20939                    };
20940                    expr = Expr::Binary {
20941                        op,
20942                        lhs: Box::new(expr),
20943                        rhs: Box::new(rhs),
20944                    };
20945                    {
20946                        return Ok(Some(expr));
20947                    }
20948                }
20949                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20950                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20951                // Lowers onto pg_is_json(x, kind); NOT wraps the
20952                // call in a logical negation.
20953                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20954                if s.eq_ignore_ascii_case("json"))
20955                {
20956                    self.advance(); // JSON
20957                    let kind = match self.peek() {
20958                        Token::Ident(s) | Token::QuotedIdent(s)
20959                            if matches!(
20960                                s.to_ascii_lowercase().as_str(),
20961                                "value" | "object" | "array" | "scalar"
20962                            ) =>
20963                        {
20964                            let k = s.to_ascii_lowercase();
20965                            self.advance();
20966                            k
20967                        }
20968                        _ => "value".to_string(),
20969                    };
20970                    let call = Expr::FunctionCall {
20971                        name: "pg_is_json".to_string(),
20972                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20973                    };
20974                    expr = if negated {
20975                        Expr::Unary {
20976                            op: UnOp::Not,
20977                            expr: Box::new(call),
20978                        }
20979                    } else {
20980                        call
20981                    };
20982                    {
20983                        return Ok(Some(expr));
20984                    }
20985                }
20986                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
20987                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
20988                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
20989                {
20990                    let form_kw = match self.peek() {
20991                        Token::Ident(s) | Token::QuotedIdent(s)
20992                            if matches!(
20993                                s.to_ascii_uppercase().as_str(),
20994                                "NFC" | "NFD" | "NFKC" | "NFKD"
20995                            ) && matches!(
20996                                self.tokens.get(self.pos + 1),
20997                                Some(Token::Ident(n) | Token::QuotedIdent(n))
20998                                    if n.eq_ignore_ascii_case("normalized")
20999                            ) =>
21000                        {
21001                            Some(s.to_ascii_uppercase())
21002                        }
21003                        _ => None,
21004                    };
21005                    let bare_normalized = form_kw.is_none()
21006                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21007                        if s.eq_ignore_ascii_case("normalized"));
21008                    if form_kw.is_some() || bare_normalized {
21009                        if form_kw.is_some() {
21010                            self.advance(); // form keyword
21011                        }
21012                        self.advance(); // NORMALIZED
21013                        let mut args = alloc::vec![expr];
21014                        if let Some(f) = form_kw {
21015                            args.push(Expr::Literal(Literal::String(f)));
21016                        }
21017                        let call = Expr::FunctionCall {
21018                            name: "is_normalized".to_string(),
21019                            args,
21020                        };
21021                        expr = if negated {
21022                            Expr::Unary {
21023                                op: UnOp::Not,
21024                                expr: Box::new(call),
21025                            }
21026                        } else {
21027                            call
21028                        };
21029                        {
21030                            return Ok(Some(expr));
21031                        }
21032                    }
21033                }
21034                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21035                // three-valued boolean tests. IS TRUE/FALSE never
21036                // return NULL, so they lower to CASE forms whose
21037                // ELSE catches the NULL branch; IS UNKNOWN on a
21038                // boolean is exactly IS NULL.
21039                if matches!(self.peek(), Token::True | Token::False)
21040                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21041                {
21042                    let tok = self.advance();
21043                    let test = match tok {
21044                        Token::True => Some(true),
21045                        Token::False => Some(false),
21046                        _ => None, // UNKNOWN
21047                    };
21048                    // v7.39 (round 328, V45) — kept as what the user
21049                    // wrote. These used to be lowered here into `CASE` /
21050                    // `IS NULL`; the semantics were right but the AST no
21051                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21052                    // was echoed back as
21053                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21054                    expr = Expr::BoolTest {
21055                        expr: Box::new(expr),
21056                        value: test,
21057                        negated,
21058                    };
21059                    {
21060                        return Ok(Some(expr));
21061                    }
21062                }
21063                if !matches!(self.peek(), Token::Null) {
21064                    return Err(self.err(format!(
21065                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21066                    if negated { " NOT" } else { "" },
21067                    self.peek()
21068                )));
21069                }
21070                self.advance();
21071                expr = Expr::IsNull {
21072                    expr: Box::new(expr),
21073                    negated,
21074                };
21075                {
21076                    return Ok(Some(expr));
21077                }
21078            }
21079        }
21080        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21081        if min_prec <= 5 {
21082            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21083            // Look one token ahead so a stray `NOT` not followed by any of
21084            // these flows through to the early return below untouched.
21085            let negated = if matches!(self.peek(), Token::Not) {
21086                let next = self.tokens.get(self.pos + 1);
21087                matches!(next, Some(Token::Between | Token::In | Token::Like))
21088                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21089                    || (self.mysql_dialect
21090                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21091                    || s.eq_ignore_ascii_case("similar"))
21092            } else {
21093                false
21094            };
21095            if negated {
21096                self.advance();
21097            }
21098            if matches!(self.peek(), Token::Between) {
21099                expr = self.parse_between_tail(expr, negated)?;
21100                {
21101                    return Ok(Some(expr));
21102                }
21103            }
21104            if matches!(self.peek(), Token::In) {
21105                if self.suppress_in_tail && !negated {
21106                    // POSITION(sub IN str) — IN belongs to the
21107                    // enclosing function syntax; stop here.
21108                    {
21109                        return Ok(None);
21110                    }
21111                }
21112                expr = self.parse_in_tail(expr, negated)?;
21113                {
21114                    return Ok(Some(expr));
21115                }
21116            }
21117            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21118            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21119            // (the SQL→regex transform runs inside, in the backtracking-
21120            // friendly shape SPG's matcher needs).
21121            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21122                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21123            {
21124                self.advance(); // SIMILAR
21125                self.advance(); // TO
21126                let pattern = self.parse_expr(6)?;
21127                let mut args = alloc::vec![expr, pattern];
21128                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21129                    self.advance();
21130                    args.push(self.parse_expr(6)?);
21131                }
21132                let call = Expr::FunctionCall {
21133                    name: "__similar_to".to_string(),
21134                    args,
21135                };
21136                expr = maybe_not(call, negated);
21137                {
21138                    return Ok(Some(expr));
21139                }
21140            }
21141            if matches!(self.peek(), Token::Like) {
21142                self.advance();
21143                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21144                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21145                    expr = q;
21146                    {
21147                        return Ok(Some(expr));
21148                    }
21149                }
21150                // Pattern at the same precedence as other comparison RHSes —
21151                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21152                let mut pattern = self.parse_expr(6)?;
21153                // `ESCAPE 'c'` — rewrite a literal pattern to the
21154                // default backslash escape at parse time. Custom
21155                // escapes on non-literal patterns would need
21156                // matcher support; error honestly.
21157                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21158                    self.advance();
21159                    let esc = self.parse_expr(6)?;
21160                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21161                }
21162                expr = Expr::Like {
21163                    expr: Box::new(expr),
21164                    pattern: Box::new(pattern),
21165                    negated,
21166                    case_insensitive: false,
21167                };
21168                {
21169                    return Ok(Some(expr));
21170                }
21171            }
21172            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21173            // keyword reaches us as a plain identifier.
21174            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21175                self.advance();
21176                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21177                    expr = q;
21178                    {
21179                        return Ok(Some(expr));
21180                    }
21181                }
21182                let pattern = self.parse_expr(6)?;
21183                expr = Expr::Like {
21184                    expr: Box::new(expr),
21185                    pattern: Box::new(pattern),
21186                    negated,
21187                    case_insensitive: true,
21188                };
21189                {
21190                    return Ok(Some(expr));
21191                }
21192            }
21193            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21194            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21195            // matches case-insensitively under the default collation, so it
21196            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21197            // `~*` operator uses, wrapped in NOT when negated.
21198            if self.mysql_dialect
21199                && matches!(self.peek(), Token::Ident(s)
21200                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21201            {
21202                self.advance();
21203                let pattern = self.parse_expr(6)?;
21204                let call = Expr::FunctionCall {
21205                    name: String::from("regexp_like"),
21206                    args: alloc::vec![
21207                        expr,
21208                        pattern,
21209                        Expr::Literal(Literal::String(String::from("i"))),
21210                    ],
21211                };
21212                return Ok(Some(maybe_not(call, negated)));
21213            }
21214        }
21215        let _ = expr;
21216        Ok(None)
21217    }
21218
21219    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21220        let mut lhs = self.parse_unary()?;
21221        let mut chain_len = 0usize;
21222        loop {
21223            // OPERATOR([schema.]op) reduces to its underlying
21224            // operator token before the normal dispatch.
21225            let explicit = self.peek_explicit_operator();
21226            let dispatch = match &explicit {
21227                Some((_, tok)) => self.binop_here(tok),
21228                None => self.binop_here(self.peek()),
21229            };
21230            let Some((op, prec)) = dispatch else {
21231                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21232                // of the symbol family. `binop_here` answers None for them
21233                // because they lower onto function calls rather than a
21234                // BinOp, and the fallback below reads `self.peek()` — the
21235                // word OPERATOR, not the operator. `pg_dump` writes every
21236                // catalog predicate this way, so its first query failed
21237                // and no dump ran:
21238                //
21239                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21240                //
21241                // Collapsing the wrapper to the operator it names puts the
21242                // token where the fallback already looks.
21243                if let Some((next, op_tok)) = explicit {
21244                    self.tokens.splice(self.pos..next, [op_tok]);
21245                }
21246                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21247                    lhs = e;
21248                    chain_len += 1;
21249                    if chain_len > MAX_BINARY_CHAIN {
21250                        return Err(self.err(alloc::format!(
21251                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21252                        )));
21253                    }
21254                    continue;
21255                }
21256                break;
21257            };
21258            if prec < min_prec {
21259                break;
21260            }
21261            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21262            // iteratively but evaluates and drops recursively;
21263            // depth beyond the budget overflows worker stacks.
21264            chain_len += 1;
21265            if chain_len > MAX_BINARY_CHAIN {
21266                return Err(self.err(alloc::format!(
21267                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21268                )));
21269            }
21270            match explicit {
21271                Some((end_pos, _)) => self.pos = end_pos,
21272                None => {
21273                    self.advance();
21274                }
21275            }
21276            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21277            // ANY is a bare ident; ALL is a reserved Token. Both
21278            // require an immediate `(` to disambiguate from
21279            // identifier columns named `any` / `all`.
21280            let any_kind = match self.peek() {
21281                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21282                    Some(false)
21283                }
21284                Token::Ident(s) | Token::QuotedIdent(s)
21285                    if (s.eq_ignore_ascii_case("any")
21286                        || s.eq_ignore_ascii_case("some")
21287                        || s.eq_ignore_ascii_case("all"))
21288                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21289                {
21290                    Some(!s.eq_ignore_ascii_case("all"))
21291                }
21292                _ => None,
21293            };
21294            if let Some(is_any) = any_kind {
21295                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21296                continue;
21297            }
21298            let rhs = self.parse_expr(prec + 1)?;
21299            lhs = Expr::Binary {
21300                lhs: Box::new(lhs),
21301                op,
21302                rhs: Box::new(rhs),
21303            };
21304        }
21305        Ok(lhs)
21306    }
21307
21308    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21309    /// and the array form.
21310    ///
21311    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21312    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21313    /// this block's `Expr` temporaries and four `format!` sites slots in
21314    /// that frame on every level of `((((1))))`, which never reaches it.
21315    #[inline(never)]
21316    fn parse_any_all_rhs(
21317        &mut self,
21318        lhs: Expr,
21319        op: BinOp,
21320        is_any: bool,
21321    ) -> Result<Expr, ParseError> {
21322        self.advance(); // ident
21323        self.advance(); // (
21324        // `x op ANY (SELECT …)` — the quantified-subquery
21325        // form. `= ANY` is exactly IN; the other operators
21326        // lower onto EXISTS over the subquery as a derived
21327        // table, comparing against its single projection
21328        // aliased __v (x's columns resolve correlated).
21329        // ALL is the negated-EXISTS complement; a NULL
21330        // element makes PG return NULL where this lowering
21331        // returns true — the NOT NULL column case (the
21332        // practical one) is exact.
21333        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21334            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21335            // legal PG too (round-151 sibling). Out-of-line
21336            // (#[inline(never)] helper) — this sits on
21337            // parse_expr's recursive frame and the two-armed
21338            // SELECT temporary blew the nesting-budget stack.
21339            let mut sub = self.parse_any_all_select_body()?;
21340            if !matches!(self.peek(), Token::RParen) {
21341                return Err(self.err(alloc::format!(
21342                    "expected ')' after ANY/ALL subquery, got {:?}",
21343                    self.peek()
21344                )));
21345            }
21346            self.advance();
21347            if sub.items.len() != 1 {
21348                return Err(self.err(alloc::format!(
21349                    "ANY/ALL subquery must return one column, got {}",
21350                    sub.items.len()
21351                )));
21352            }
21353            if is_any && matches!(op, BinOp::Eq) {
21354                return Ok(Expr::InSubquery {
21355                    expr: Box::new(lhs),
21356                    subquery: Box::new(sub),
21357                    negated: false,
21358                });
21359            }
21360            // The engine's subquery resolvers materialise
21361            // the single-column result into an ARRAY the
21362            // existing AnyAll three-valued eval consumes.
21363            return Ok(Expr::AnyAll {
21364                expr: Box::new(lhs),
21365                op,
21366                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21367                is_any,
21368            });
21369        }
21370        let arr = self.parse_expr(0)?;
21371        if !matches!(self.peek(), Token::RParen) {
21372            return Err(self.err(alloc::format!(
21373                "expected ')' after ANY/ALL argument, got {:?}",
21374                self.peek()
21375            )));
21376        }
21377        self.advance();
21378        Ok(Expr::AnyAll {
21379            expr: Box::new(lhs),
21380            op,
21381            array: Box::new(arr),
21382            is_any,
21383        })
21384    }
21385
21386    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21387    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21388    #[inline(never)]
21389    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21390        self.advance();
21391        let e = self.parse_expr(9)?;
21392        Ok(build_center_call(e))
21393    }
21394
21395    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21396    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21397    /// unary minus.
21398    ///
21399    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21400    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21401    /// the Expr-sized local stays out of that frame.
21402    #[inline(never)]
21403    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21404        self.advance();
21405        let e = self.parse_expr(9)?;
21406        Ok(Expr::FunctionCall {
21407            name: alloc::string::String::from(name),
21408            args: alloc::vec![e],
21409        })
21410    }
21411
21412    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21413    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21414    #[inline(never)]
21415    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21416        self.advance();
21417        let e = self.parse_expr(9)?;
21418        Ok(Expr::FunctionCall {
21419            name: alloc::string::String::from(if vertical {
21420                "isvertical"
21421            } else {
21422                "ishorizontal"
21423            }),
21424            args: alloc::vec![e],
21425        })
21426    }
21427
21428    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21429    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21430    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21431    #[inline(never)]
21432    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21433        self.advance();
21434        let e = self.parse_expr(9)?;
21435        Ok(Expr::Cast {
21436            expr: Box::new(e),
21437            target: CastTarget::Named("binary".to_string()),
21438        })
21439    }
21440
21441    /// The prefix operators that share one shape: take the token, parse
21442    /// an operand at `prec`, wrap it.
21443    ///
21444    /// `#[inline(never)]`, and one function instead of five arms, for the
21445    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21446    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21447    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21448    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21449    /// five `Expr`-sized locals per level for them anyway.
21450    #[inline(never)]
21451    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21452        self.advance();
21453        let e = self.parse_expr(prec)?;
21454        Ok(Expr::Unary {
21455            op,
21456            expr: Box::new(e),
21457        })
21458    }
21459
21460    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21461    /// and separate from it because of the literal folding below and the
21462    /// `format!` temporaries that folding needs.
21463    #[inline(never)]
21464    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21465        self.advance();
21466        // v7.39 (round 549) — fold the sign into an integer literal that
21467        // only fits once it is negative.
21468        //
21469        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21470        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21471        // folds the sign first, so `-9223372036854775808` is a bigint
21472        // there — and `-9223372036854775808 - 1` raises "bigint out of
21473        // range" where SPG quietly answered -9223372036854775809, a value
21474        // no bigint can hold. The arithmetic itself was already checked;
21475        // only the literal's type was wrong.
21476        if let Token::Numeric(lit) = self.peek()
21477            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21478        {
21479            self.advance();
21480            return Ok(Expr::Literal(Literal::Integer(folded)));
21481        }
21482        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21483        // `<->` slotted into 5 and arithmetic shifted up).
21484        let e = self.parse_expr(9)?;
21485        Ok(Expr::Unary {
21486            op: UnOp::Neg,
21487            expr: Box::new(e),
21488        })
21489    }
21490
21491    /// tsquery `!!` prefix negation, lowered to the catalog function.
21492    /// Binds like unary minus. Out-of-line for the frame reason on
21493    /// `parse_unary_op`.
21494    #[inline(never)]
21495    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21496        self.advance();
21497        let e = self.parse_expr(9)?;
21498        Ok(Expr::FunctionCall {
21499            name: String::from("tsquery_not"),
21500            args: alloc::vec![e],
21501        })
21502    }
21503
21504    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21505        match self.peek() {
21506            // NOT binds tighter than AND / XOR / OR but looser than
21507            // comparisons — its operand takes everything ≥ the comparison
21508            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21509            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21510            // was rung 3, behaviour-identical when 3 was unused; AND now
21511            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21512            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21513            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21514            // The body is out-of-line: `parse_unary` is one of the three
21515            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21516            // inline arm here overflowed the native stack in
21517            // `nesting_budget_errors_cleanly` — the guard test caught it,
21518            // exactly as the eval-side cliff did in rounds 346 and 351.
21519            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21520                self.parse_binary_prefix()
21521            }
21522            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21523            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21524            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21525            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21526            Token::Minus => self.parse_prefix_minus(),
21527            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21528            // worked only because the lexer reads it as one signed literal;
21529            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21530            // PG18 and MariaDB take all of them. Binds like unary minus.
21531            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21532            // Bitwise NOT binds like unary minus.
21533            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21534            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21535            // "center of" operator; desugars to center(x). The whole arm
21536            // is out-of-line: parse_unary sits on the per-nesting-level
21537            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21538            // Expr-sized local may live in this frame.
21539            Token::TsMatch => self.parse_prefix_center(),
21540            // v7.39 (round 508) — the prefix operators that are named
21541            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21542            // is length. Out-of-line for the same nesting-frame reason as
21543            // parse_prefix_center — parse_unary sits on the recursive cycle
21544            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21545            // live in this frame.
21546            Token::At => self.parse_prefix_call("abs"),
21547            Token::Hash => self.parse_prefix_call("npoints"),
21548            Token::AtMinusAt => self.parse_prefix_call("length"),
21549            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21550            // "is horizontal" (lseg / line); desugars to the existing
21551            // isvertical()/ishorizontal() functions. Out-of-line for the
21552            // same nesting-frame reason as parse_prefix_center.
21553            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21554            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21555            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21556            _ => self.parse_atom(),
21557        }
21558    }
21559
21560    /// Parse a parenthesised scalar subquery body after the caller has consumed
21561    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21562    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21563    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21564    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21565    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21566    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21567    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21568    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21569    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21570    /// tips the deep-nesting test into a stack overflow).
21571    #[inline(never)]
21572    fn array_subquery_ahead(&self) -> bool {
21573        if !matches!(self.peek(), Token::LParen) {
21574            return false;
21575        }
21576        matches!(
21577            self.tokens.get(self.pos + 1),
21578            Some(Token::Select | Token::Values)
21579        ) || matches!(
21580            self.tokens.get(self.pos + 1),
21581            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21582        )
21583    }
21584
21585    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21586    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21587    /// locals stay off parse_atom's recursive frame (round 105).
21588    #[inline(never)]
21589    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21590        self.advance(); // consume `[`
21591        let mut items: Vec<Expr> = Vec::new();
21592        if !matches!(self.peek(), Token::RBracket) {
21593            loop {
21594                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21595                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21596                if matches!(self.peek(), Token::LBracket) {
21597                    items.push(self.parse_array_bracket_body()?);
21598                } else {
21599                    items.push(self.parse_expr(0)?);
21600                }
21601                match self.peek() {
21602                    Token::Comma => {
21603                        self.advance();
21604                    }
21605                    Token::RBracket => break,
21606                    other => {
21607                        return Err(self.err(alloc::format!(
21608                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21609                        )));
21610                    }
21611                }
21612            }
21613        }
21614        self.advance(); // consume `]`
21615        Ok(Expr::Array(items))
21616    }
21617
21618    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21619    /// is already consumed; the current token is `(`. Desugars to a scalar
21620    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21621    /// the subquery's single-column rows in order — reusing the existing
21622    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21623    /// keeps the large `Statement` local off parse_atom's recursive frame.
21624    #[inline(never)]
21625    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21626        self.advance(); // consume `(`
21627        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21628            if w.eq_ignore_ascii_case("with"));
21629        let sub = if is_with {
21630            self.advance(); // WITH
21631            self.parse_with_cte_then_select()?
21632        } else {
21633            self.parse_select_stmt()?
21634        };
21635        if !matches!(self.peek(), Token::RParen) {
21636            return Err(self.err(alloc::format!(
21637                "expected ')' to close ARRAY(subquery), got {:?}",
21638                self.peek()
21639            )));
21640        }
21641        self.advance(); // consume `)`
21642        // Reuse the parser to build the array_agg wrapper from the subquery's
21643        // canonical text — avoids hand-constructing the derived-table AST.
21644        let wrapper = alloc::format!(
21645            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21646        );
21647        let stmt = parse_statement(&wrapper)
21648            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21649        let Statement::Select(sel) = stmt else {
21650            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21651        };
21652        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21653    }
21654
21655    #[inline(never)]
21656    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21657        let inner = if is_with {
21658            self.advance(); // WITH
21659            self.parse_with_cte_then_select()?
21660        } else {
21661            self.parse_select_stmt()?
21662        };
21663        match self.advance() {
21664            Token::RParen => {
21665                let Statement::Select(s) = inner else {
21666                    return Err(ParseError {
21667                        message: "scalar subquery body must be a SELECT".into(),
21668                        token_pos: self.consumed_pos(),
21669                    });
21670                };
21671                Ok(Expr::ScalarSubquery(Box::new(s)))
21672            }
21673            other => Err(ParseError {
21674                message: format!("expected ')' after scalar subquery, got {other:?}"),
21675                token_pos: self.consumed_pos(),
21676            }),
21677        }
21678    }
21679
21680    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21681    /// literals. The lexer splits them into an ident + string; recombine
21682    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21683    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21684    /// frame for the `body` / `bits` strings and their char loops (the
21685    /// round-367 frame cliff, M20).
21686    #[inline(never)]
21687    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21688        let is_hex = match self.peek() {
21689            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21690            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21691            _ => return None,
21692        };
21693        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21694            return None;
21695        }
21696        self.advance();
21697        let Token::String(body) = self.advance() else {
21698            unreachable!("guarded above");
21699        };
21700        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21701        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21702        // (hex pairs, even count required — MariaDB errors on an odd
21703        // count); `b'1010'` packs its bits big-endian, left-padded to a
21704        // byte. Lower both onto the bytea cast.
21705        if self.mysql_dialect {
21706            if is_hex {
21707                if body.len() % 2 == 1 {
21708                    return Some(Err(self.err(alloc::format!(
21709                        "invalid hex string literal X'{body}': odd digit count"
21710                    ))));
21711                }
21712                for c in body.chars() {
21713                    if !c.is_ascii_hexdigit() {
21714                        return Some(Err(
21715                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21716                        ));
21717                    }
21718                }
21719                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21720            }
21721            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21722                return Some(Err(
21723                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21724                ));
21725            }
21726            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21727        }
21728        let bits = if is_hex {
21729            let mut out = String::with_capacity(body.len() * 4);
21730            for c in body.chars() {
21731                let Some(d) = c.to_digit(16) else {
21732                    return Some(Err(self.err(alloc::format!(
21733                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21734                    ))));
21735                };
21736                out.push_str(&alloc::format!("{d:04b}"));
21737            }
21738            out
21739        } else {
21740            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21741                return Some(Err(self.err(alloc::format!(
21742                    "invalid binary digit {bad:?} in B'…' bit string"
21743                ))));
21744            }
21745            body
21746        };
21747        // Route through the postfix-cast loop so a chained cast like
21748        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21749        // of erroring at the `::`.
21750        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21751        // literal keeps its exact length, while an explicit `::bit` cast is
21752        // bit(1) with pad/truncate semantics (PG).
21753        Some(self.finish_postfix_casts(Expr::Cast {
21754            expr: Box::new(Expr::Literal(Literal::String(bits))),
21755            target: CastTarget::Named("__bit_literal".to_string()),
21756        }))
21757    }
21758
21759    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21760        if let Some(res) = self.try_parse_bit_string_literal() {
21761            return res;
21762        }
21763        let tok_pos = self.pos;
21764        match self.advance() {
21765            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21766            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21767            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21768            // carrying the source mantissa + scale so no precision is lost. A
21769            // literal too wide for i128 falls back to double precision.
21770            // Out-of-line (#[inline(never)]) — this arm sits on the
21771            // parse_expr recursion chain; its expansion locals must not
21772            // widen the recursive frame (debug frame-cliff discipline).
21773            Token::Numeric(s) => match numeric_token_to_literal(s) {
21774                Ok(lit) => Ok(Expr::Literal(lit)),
21775                Err(msg) => Err(self.err(msg)),
21776            },
21777            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21778            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21779            // (the lexer only emits this token in the MySQL dialect). Lower
21780            // onto the existing bytea cast; out-of-line to keep this arm off
21781            // the parse recursion frame.
21782            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21783            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21784            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21785            Token::Null => Ok(Expr::Literal(Literal::Null)),
21786            // v6.1.1 — `$N` placeholder. The actual Value lookup
21787            // happens in the engine eval path against the prepared-
21788            // statement bind buffer.
21789            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21790            Token::LParen => {
21791                // v4.10: `(SELECT ...)` in expression position is a
21792                // scalar subquery; otherwise it's a parenthesised
21793                // expression. Peek for SELECT keyword to dispatch.
21794                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21795                // lexes as Ident("with") (not a reserved token). The subquery body
21796                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21797                // so its large `Statement` local stays out of parse_atom's stack
21798                // frame — parse_atom is on the recursive `((…))` cycle and the
21799                // nesting budget is tuned to its frame size).
21800                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21801                    if s.eq_ignore_ascii_case("with"));
21802                if matches!(self.peek(), Token::Select) || is_with {
21803                    self.parse_paren_scalar_subquery(is_with)
21804                } else {
21805                    let e = self.parse_expr(0)?;
21806                    // `(a, b, …)` — a row constructor. Valid only
21807                    // in front of a comparison operator or [NOT]
21808                    // IN; both expand at parse time (lexicographic
21809                    // comparison / OR'd row equalities).
21810                    if matches!(self.peek(), Token::Comma) {
21811                        let mut row = alloc::vec![e];
21812                        while matches!(self.peek(), Token::Comma) {
21813                            self.advance();
21814                            row.push(self.parse_expr(0)?);
21815                        }
21816                        if !matches!(self.peek(), Token::RParen) {
21817                            return Err(self.err(alloc::format!(
21818                                "expected ')' after row constructor, got {:?}",
21819                                self.peek()
21820                            )));
21821                        }
21822                        self.advance();
21823                        // A bare `(a, b, …)` row constructor can carry postfix
21824                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21825                        // early return here skips parse_atom's tail postfix
21826                        // pass, so fold casts in explicitly. For the
21827                        // comparison / predicate forms nothing postfix follows,
21828                        // so this is a no-op.
21829                        return self
21830                            .parse_row_comparison_tail(row)
21831                            .and_then(|e| self.finish_postfix_casts(e));
21832                    }
21833                    match self.advance() {
21834                        Token::RParen => Ok(e),
21835                        other => Err(ParseError {
21836                            message: format!("expected ')', got {other:?}"),
21837                            token_pos: self.consumed_pos(),
21838                        }),
21839                    }
21840                }
21841            }
21842            Token::LBracket => self.parse_vector_literal_body(),
21843            Token::Extract => self.parse_extract_atom(),
21844            Token::Interval => self.parse_interval_atom(),
21845            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21846            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21847            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21848            // expression position calling the PG `left(string, n)` /
21849            // `right(string, n)` function; rebuild the AST as a regular
21850            // function call so the engine's apply_function dispatch picks
21851            // it up. Delegated to a #[inline(never)] helper so its locals
21852            // don't bloat this recursive `parse_atom` frame (the nesting
21853            // budget in `enter_nested` is tuned to parse_atom's size).
21854            Token::Left if matches!(self.peek(), Token::LParen) => {
21855                self.parse_lr_string_function_call("left")
21856            }
21857            Token::Right if matches!(self.peek(), Token::LParen) => {
21858                self.parse_lr_string_function_call("right")
21859            }
21860            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21861            // token; we match on the bare ident. NOT is a token
21862            // (consumed in the comparison rung), but `EXISTS (...)`
21863            // at the top of an expression starts here.
21864            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21865                self.parse_exists_atom(false)
21866            }
21867            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21868            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21869            // CASE is a bare ident; we dispatch on lowercase match.
21870            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21871                self.parse_case_atom()
21872            }
21873            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21874            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21875            // '…'`. Lower onto the ::cast node so the existing
21876            // runtime text→date/timestamp paths do the parsing. The
21877            // string must follow immediately, else the ident stays a
21878            // plain column reference.
21879            Token::Ident(s)
21880                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21881                    && matches!(self.peek(), Token::String(_)) =>
21882            {
21883                let target =
21884                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21885                let Token::String(lit) = self.advance() else {
21886                    unreachable!("peek guaranteed a string token");
21887                };
21888                Ok(Expr::Cast {
21889                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21890                    target,
21891                })
21892            }
21893            // v7.39 (round 221) — the SQL-standard long spellings:
21894            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21895            // TIME ZONE '…'`. Consume the modifier and lower to the same
21896            // typed-literal cast (`timetz` / `timestamptz` for WITH).
21897            Token::Ident(s)
21898                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21899                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21900                        || w.eq_ignore_ascii_case("without"))
21901                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21902                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21903                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21904            {
21905                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21906                self.advance(); // WITH / WITHOUT
21907                self.advance(); // TIME
21908                self.advance(); // ZONE
21909                let Token::String(lit) = self.advance() else {
21910                    unreachable!("guard checked a string token");
21911                };
21912                let base = s.to_ascii_lowercase();
21913                let target = match (base.as_str(), with_tz) {
21914                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21915                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21916                    (_, true) => CastTarget::Timestamptz,
21917                    (_, false) => CastTarget::Timestamp,
21918                };
21919                Ok(Expr::Cast {
21920                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21921                    target,
21922                })
21923            }
21924            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21925            // gathers the subquery's single-column rows (in its row order)
21926            // into an array. Desugared to `array_agg` over the subquery as a
21927            // derived table; out-of-line to keep parse_atom's frame small (it
21928            // sits on the recursive nesting-budget cycle).
21929            Token::Ident(s) | Token::QuotedIdent(s)
21930                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21931            {
21932                self.parse_array_subquery()
21933            }
21934            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21935            // is not a reserved token; we match by case-insensitive
21936            // ident. The opening `[` must follow immediately. v7.39 (read01
21937            // round 105) — the body moved out-of-line so its `Vec`/loop locals
21938            // leave parse_atom's frame (which sits on the nesting-budget cycle).
21939            Token::Ident(s) | Token::QuotedIdent(s)
21940                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21941            {
21942                self.parse_array_literal_body()
21943            }
21944            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21945            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21946            // We special-case before the generic ident dispatch so
21947            // the AGAINST clause never reaches the function-call
21948            // loop (which would mis-read `(cols) AGAINST` as a
21949            // call with no trailing modifier). The shape is
21950            // rewritten to a Boolean OR over per-column
21951            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21952            // term)` so the existing FTS evaluator handles
21953            // semantics — the fulltext-GIN built at CREATE TABLE
21954            // time is currently a "real index that survives dump
21955            // round-trip"; the planner hook that actually uses
21956            // it for posting-list intersection lands in a later
21957            // sub-phase (Phase 2.2b) without touching this surface.
21958            Token::Ident(s) | Token::QuotedIdent(s)
21959                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21960            {
21961                self.parse_match_against_atom()
21962            }
21963            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21964            // v7.37.43-T4 — PG-unreserved keywords are legal column /
21965            // alias names in expression context too. `release` appears
21966            // in sentori `0003_partition_events.sql` as both a column
21967            // reference (SELECT … release …) and an INSERT column list
21968            // entry. Mirrors `expect_ident_like`'s expansion of the
21969            // identifier set.
21970            other if unreserved_keyword_text(&other).is_some() => {
21971                let s = unreserved_keyword_text(&other).unwrap();
21972                self.finish_ident_atom(s)
21973            }
21974            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21975            // only inside `SET` before, so `SELECT @@autocommit` — which
21976            // every MySQL connector asks at handshake — was a parse error.
21977            // MariaDB accepts the bare, `@@session.` and `@@global.`
21978            // spellings alike and answers from the session's own value.
21979            Token::SessionVar(v) => {
21980                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21981                // has nothing to do with a `@@` engine setting: its own
21982                // per-session namespace, and an unset one reads NULL instead
21983                // of raising. Stripping every `@` (as this did) made `@x` and
21984                // `@@x` the same node, so `SELECT @x` answered "Unknown
21985                // system variable".
21986                Ok(variable_ref_atom(&v))
21987            }
21988            other => Err(ParseError {
21989                message: format!("unexpected token {other:?} in expression"),
21990                token_pos: tok_pos,
21991            }),
21992        }
21993        // After parsing the atom, fold any postfix `::vector` casts.
21994        .and_then(|atom| self.finish_postfix_casts(atom))
21995    }
21996
21997    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
21998    /// Both bind tighter than any binary op.
21999    /// Shared cast-target parser for postfix `::TYPE` and the
22000    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22001    /// If the next tokens are `( N )`, consume them and return the canonical
22002    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22003    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22004    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22005        if !matches!(self.peek(), Token::LParen) {
22006            return None;
22007        }
22008        self.advance(); // (
22009        let n = match self.advance() {
22010            Token::Integer(n) => n,
22011            _ => return Some(base.to_string()), // malformed → drop precision
22012        };
22013        if matches!(self.peek(), Token::RParen) {
22014            self.advance();
22015        }
22016        Some(alloc::format!("{base}({n})"))
22017    }
22018
22019    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22020        let target = match self.advance() {
22021            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22022                "int" | "integer" | "int4" => {
22023                    if matches!(self.peek(), Token::LBracket)
22024                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22025                    {
22026                        self.advance();
22027                        self.advance();
22028                        CastTarget::IntArray
22029                    } else {
22030                        CastTarget::Int
22031                    }
22032                }
22033                "bigint" | "int8" => {
22034                    if matches!(self.peek(), Token::LBracket)
22035                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22036                    {
22037                        self.advance();
22038                        self.advance();
22039                        CastTarget::BigIntArray
22040                    } else {
22041                        CastTarget::BigInt
22042                    }
22043                }
22044                "float" | "double" => CastTarget::Float,
22045                "text" => {
22046                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22047                    if matches!(self.peek(), Token::LBracket)
22048                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22049                    {
22050                        self.advance();
22051                        self.advance();
22052                        CastTarget::TextArray
22053                    } else {
22054                        CastTarget::Text
22055                    }
22056                }
22057                "bool" | "boolean" => CastTarget::Bool,
22058                "vector" => CastTarget::Vector,
22059                "date" => CastTarget::Date,
22060                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22061                // seconds precision through the Named path (the engine rounds
22062                // the sub-second field); bare `::timestamp` keeps the fast arm.
22063                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22064                    Some(named) => CastTarget::Named(named),
22065                    None => CastTarget::Timestamp,
22066                },
22067                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22068                    Some(named) => CastTarget::Named(named),
22069                    None => CastTarget::Timestamptz,
22070                },
22071                "interval" => CastTarget::Interval,
22072                "json" => CastTarget::Json,
22073                "jsonb" => CastTarget::Jsonb,
22074                // v7.39 (round 694) — these have dedicated CastTarget
22075                // variants, so they never reached the postfix `[]` handling
22076                // further down and `::regtype[]` was a SYNTAX error at the
22077                // `]`. PG has an array type for every scalar; take the
22078                // suffix here and hand the canonical `<ty>_array` name to
22079                // the engine, the same shape every other array cast uses.
22080                "regtype" if self.peek_postfix_array_brackets() => {
22081                    self.advance();
22082                    self.advance();
22083                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22084                }
22085                "regclass" if self.peek_postfix_array_brackets() => {
22086                    self.advance();
22087                    self.advance();
22088                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22089                }
22090                "regtype" => CastTarget::RegType,
22091                "regclass" => CastTarget::RegClass,
22092                // v7.12.0 — `::tsvector` / `::tsquery`.
22093                // Engine decodes the LHS text via the PG
22094                // external form parser.
22095                // v7.39 (round 352, M8) — MySQL's own cast targets.
22096                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22097                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22098                // such type, so they are taken only in that dialect and
22099                // fall through to the "type does not exist" arm otherwise.
22100                "signed" | "unsigned" if self.mysql_dialect => {
22101                    if matches!(self.peek(), Token::Ident(k)
22102                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22103                    {
22104                        self.advance();
22105                    }
22106                    CastTarget::Named(s.to_ascii_lowercase())
22107                }
22108                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22109                // in MySQL: MariaDB answers '123' where the SQL-standard
22110                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22111                // Truncating a number to its first digit is a wrong answer
22112                // with no error, so the MySQL session gets MySQL's reading.
22113                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22114                    CastTarget::Text
22115                }
22116                "tsvector" => CastTarget::TsVector,
22117                "tsquery" => CastTarget::TsQuery,
22118                // v7.17.0 — `::uuid`. Engine decodes the LHS
22119                // text via `spg_storage::parse_uuid_str`.
22120                "uuid" => CastTarget::Uuid,
22121                // v7.18 — `::bytea`. Engine decodes the LHS
22122                // text via the PG hex form (`'\xdeadbeef'`)
22123                // or escape form (`'\\x05\\x00'`). Closes
22124                // mailrs D-pre #3 reverse-acceptance gap.
22125                "bytea" => CastTarget::Bytea,
22126                // v7.37.5 ship triage — generic typed-cast escape.
22127                // Anything the long-tail PG type ident table knows
22128                // about(network/bit/geometry/multirange/etc.)flows
22129                // through `CastTarget::Named(canonical)`; the engine
22130                // resolves via `column_type_to_data_type` and dispatches
22131                // through the typed `coerce_value` path. Truly
22132                // unrecognised idents still hit the error arm below
22133                // because the engine rejects them.
22134                other => {
22135                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22136                    // `::varchar(255)`, etc. Capture into the canonical
22137                    // `name(p,s)` form so `type_name_to_data_type` can
22138                    // reconstruct the `DataType::Numeric { precision,
22139                    // scale }` (and similar param-carrying types).
22140                    let mut name = other.to_string();
22141                    // v7.39 (round 281) — `::bit varying(3)` is two
22142                    // words; fold the tail in so the typmod reaches the
22143                    // type resolver instead of tripping the parser.
22144                    if name.eq_ignore_ascii_case("bit")
22145                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22146                    {
22147                        self.advance();
22148                        name = alloc::string::String::from("varbit");
22149                    }
22150                    // v7.39 (round 613) — `::character varying` is the same
22151                    // two-word shape and had no fold, so the `varying` was
22152                    // left behind and the cast became a bare `character`,
22153                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22154                    // `a` where PG answers `ab`. Silently, and for a spelling
22155                    // pg_dump writes.
22156                    if name.eq_ignore_ascii_case("character")
22157                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22158                    {
22159                        self.advance();
22160                        name = alloc::string::String::from("varchar");
22161                    }
22162                    if matches!(self.peek(), Token::LParen) {
22163                        let mut buf = alloc::string::String::from("(");
22164                        let mut depth = 0usize;
22165                        loop {
22166                            match self.advance() {
22167                                Token::LParen => {
22168                                    depth += 1;
22169                                    if depth > 1 {
22170                                        buf.push('(');
22171                                    }
22172                                }
22173                                Token::RParen => {
22174                                    depth -= 1;
22175                                    if depth == 0 {
22176                                        buf.push(')');
22177                                        break;
22178                                    }
22179                                    buf.push(')');
22180                                }
22181                                Token::Comma => buf.push(','),
22182                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22183                                // v7.39 (round 273) — a minus used to fall
22184                                // into the catch-all below and vanish, so
22185                                // `::numeric(10,-2)` reached the engine as
22186                                // the text `numeric(10,2)` and silently
22187                                // rounded to two DECIMALS instead of to
22188                                // hundreds. A dropped token is not a
22189                                // no-op when it carries a sign.
22190                                Token::Minus => buf.push('-'),
22191                                Token::Eof => break,
22192                                _ => {}
22193                            }
22194                        }
22195                        name.push_str(&buf);
22196                    }
22197                    // Optional postfix `[]` widens to the array form —
22198                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22199                    // The engine's `type_name_to_data_type` recognises
22200                    // the canonical `<ty>_array` form.
22201                    if matches!(self.peek(), Token::LBracket)
22202                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22203                    {
22204                        self.advance();
22205                        self.advance();
22206                        name.push_str("_array");
22207                    }
22208                    CastTarget::Named(name)
22209                }
22210            },
22211            Token::Interval => CastTarget::Interval,
22212            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22213            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22214            // = char(1)); other quoted names resolve like idents.
22215            Token::QuotedIdent(q) => {
22216                if q.eq_ignore_ascii_case("char") {
22217                    CastTarget::Named("char1".into())
22218                } else {
22219                    CastTarget::Named(q.to_ascii_lowercase())
22220                }
22221            }
22222            other => {
22223                return Err(ParseError {
22224                    message: format!("expected type ident after `::`, got {other:?}"),
22225                    token_pos: self.consumed_pos(),
22226                });
22227            }
22228        };
22229        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22230        // target to its array sibling. Closed-enum arms (Bool /
22231        // SmallInt / Numeric / Float / Date / …) didn't carry the
22232        // explicit widening that Text / Int / BigInt did, so
22233        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22234        // error. The widening here mirrors the per-arm Text /
22235        // Int / BigInt logic above + folds the new ζ-A first-class
22236        // types through `CastTarget::Named("<ty>_array")`.
22237        if matches!(self.peek(), Token::LBracket)
22238            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22239        {
22240            let widened = match &target {
22241                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22242                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22243                // v7.39 (round 326, V43) — the two temporal types stay
22244                // distinct. Both used to widen to `timestamptz_array`, so
22245                // `::timestamp[]` named the wrong target in its own error
22246                // message and lost the zone-less identity on the way.
22247                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22248                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22249                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22250                CastTarget::Json | CastTarget::Jsonb => {
22251                    Some(CastTarget::Named("jsonb_array".to_string()))
22252                }
22253                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22254                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22255                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22256                CastTarget::Named(name) => {
22257                    let mut a = name.clone();
22258                    a.push_str("_array");
22259                    Some(CastTarget::Named(a))
22260                }
22261                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22262                // RegType / RegClass / TextArray / IntArray /
22263                // BigIntArray already finalised — leave as is.
22264                _ => None,
22265            };
22266            if let Some(w) = widened {
22267                self.advance();
22268                self.advance();
22269                return Ok(w);
22270            }
22271        }
22272        Ok(target)
22273    }
22274
22275    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22276        loop {
22277            // v7.38 (read01, T9) — composite field access `(expr).field`.
22278            // A bare `a.b` is consumed as a qualified column inside the ident
22279            // atom, so a Dot only survives to this postfix position when the
22280            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22281            // `.*` whole-row expansion is not handled here (projection-level).
22282            if matches!(self.peek(), Token::Dot)
22283                && matches!(
22284                    self.tokens.get(self.pos + 1),
22285                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22286                )
22287            {
22288                self.advance(); // .
22289                let field = match self.advance() {
22290                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22291                    other => {
22292                        return Err(
22293                            self.err(format!("expected a field name after '.', got {other:?}"))
22294                        );
22295                    }
22296                };
22297                expr = Expr::FieldAccess {
22298                    base: Box::new(expr),
22299                    field,
22300                };
22301                continue;
22302            }
22303            if matches!(self.peek(), Token::DoubleColon) {
22304                self.advance();
22305                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22306                // target set to include INTERVAL (reserved Token),
22307                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22308                // mailrs follow-up H3a + H3b.
22309                let target = self.parse_cast_target()?;
22310                expr = Expr::Cast {
22311                    expr: Box::new(expr),
22312                    target,
22313                };
22314                continue;
22315            }
22316            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22317            // returns NULL for out-of-range. Multiple subscripts
22318            // chain: `a[i][j]` parses left-to-right.
22319            if matches!(self.peek(), Token::LBracket) {
22320                self.advance();
22321                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22322                // bare index stays a subscript.
22323                let lo = if matches!(self.peek(), Token::Colon) {
22324                    None
22325                } else {
22326                    Some(self.parse_expr(0)?)
22327                };
22328                if matches!(self.peek(), Token::Colon) {
22329                    self.advance();
22330                    let hi = if matches!(self.peek(), Token::RBracket) {
22331                        None
22332                    } else {
22333                        Some(Box::new(self.parse_expr(0)?))
22334                    };
22335                    if !matches!(self.peek(), Token::RBracket) {
22336                        return Err(self.err(alloc::format!(
22337                            "expected ']' after array slice, got {:?}",
22338                            self.peek()
22339                        )));
22340                    }
22341                    self.advance();
22342                    expr = Expr::ArraySlice {
22343                        target: Box::new(expr),
22344                        lo: lo.map(Box::new),
22345                        hi,
22346                    };
22347                    continue;
22348                }
22349                let index = lo.expect("non-colon branch parsed an index");
22350                if !matches!(self.peek(), Token::RBracket) {
22351                    return Err(self.err(alloc::format!(
22352                        "expected ']' after array index, got {:?}",
22353                        self.peek()
22354                    )));
22355                }
22356                self.advance();
22357                expr = Expr::ArraySubscript {
22358                    target: Box::new(expr),
22359                    index: Box::new(index),
22360                };
22361                continue;
22362            }
22363            // `expr AT TIME ZONE zone` — lowers to PG's own function
22364            // form timezone(zone, expr); the scalar implements the
22365            // offset shift (named zones error there — no tzdata).
22366            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22367                && matches!(self.tokens.get(self.pos + 1),
22368                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22369                && matches!(self.tokens.get(self.pos + 2),
22370                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22371            {
22372                self.advance(); // AT
22373                self.advance(); // TIME
22374                self.advance(); // ZONE
22375                // Zone at comparison precedence so AND/OR stay out.
22376                let zone = self.parse_expr(6)?;
22377                expr = Expr::FunctionCall {
22378                    name: "timezone".to_string(),
22379                    args: alloc::vec![zone, expr],
22380                };
22381                continue;
22382            }
22383            // `expr COLLATE "name"` — SPG's single text ordering IS
22384            // byte order, i.e. the C collation. The byte-order
22385            // spellings absorb as no-ops; a locale collation would
22386            // silently sort differently from PG, so it errors
22387            // honestly instead.
22388            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22389                self.advance();
22390                let mut cname = match self.advance() {
22391                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22392                    other => {
22393                        return Err(self.err(alloc::format!(
22394                            "expected collation name after COLLATE, got {other:?}"
22395                        )));
22396                    }
22397                };
22398                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22399                // is how `pg_dump` writes the default one:
22400                // `… COLLATE pg_catalog.default`. Reading a single token
22401                // left the SCHEMA as the name, so the clause was refused
22402                // as an unsupported locale collation and no dump ran.
22403                if matches!(self.peek(), Token::Dot) {
22404                    self.advance();
22405                    cname = match self.advance() {
22406                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22407                        // `default` lexes as a KEYWORD, and it is the name
22408                        // pg_dump writes — the same trap round 535 hit with
22409                        // TABLE / INDEX / FULL.
22410                        Token::Default => alloc::string::String::from("default"),
22411                        other => {
22412                            return Err(self.err(alloc::format!(
22413                                "expected collation name after COLLATE, got {other:?}"
22414                            )));
22415                        }
22416                    };
22417                }
22418                let lc = cname.to_ascii_lowercase();
22419                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22420                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22421                // family / `binary`) forces byte-wise, which is exactly
22422                // what `BINARY expr` does — lower onto that so every fold
22423                // site (comparison, LIKE, ORDER BY) suppresses via
22424                // `is_binary_coerced`. A `_ci` family override folds, and
22425                // under the MySQL dialect the default already folds, so it
22426                // absorbs as a no-op; likewise the C / byte-order spellings.
22427                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22428                    expr = Expr::Cast {
22429                        expr: alloc::boxed::Box::new(expr),
22430                        target: CastTarget::Named("binary".to_string()),
22431                    };
22432                    continue;
22433                }
22434                let mysql_ci = self.mysql_dialect
22435                    && (lc.ends_with("_ci")
22436                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22437                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22438                // goes to the lowering channel, the byte-order spellings
22439                // included. Round 691 recorded only the names the old
22440                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22441                // absorbed as a no-op — and once a column could declare a
22442                // collation, absorbing the clause meant the COLUMN's
22443                // collation won where the query had asked for bytes.
22444                if self.in_order_by_key && !mysql_ci {
22445                    self.order_key_collation = Some(cname);
22446                    continue;
22447                }
22448                if !matches!(
22449                    lc.as_str(),
22450                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22451                ) && !mysql_ci
22452                {
22453                    return Err(self.err(alloc::format!(
22454                        "COLLATE {cname:?}: SPG orders text by bytes (the C \
22455                         collation); locale collations are not supported yet — \
22456                         use COLLATE \"C\" or drop the clause"
22457                    )));
22458                }
22459                continue;
22460            }
22461            return Ok(expr);
22462        }
22463    }
22464
22465    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22466    /// the first token that is not one. Schema qualifiers collapse to the
22467    /// last part, which is what every other name path here does (SPG is
22468    /// single-schema).
22469    fn take_comma_separated_names(&mut self) -> Vec<String> {
22470        let mut out = Vec::new();
22471        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22472            self.advance();
22473            let mut last = n;
22474            while matches!(self.peek(), Token::Dot) {
22475                self.advance();
22476                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22477                    last = t;
22478                }
22479            }
22480            out.push(last);
22481            if matches!(self.peek(), Token::Comma) {
22482                self.advance();
22483            } else {
22484                break;
22485            }
22486        }
22487        out
22488    }
22489
22490    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22491    ///
22492    /// The general cast-target path tests this inline; the types with their
22493    /// own `CastTarget` variant need it as a guard on their match arm,
22494    /// which is what this exists for.
22495    fn peek_postfix_array_brackets(&self) -> bool {
22496        matches!(self.peek(), Token::LBracket)
22497            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22498    }
22499
22500    /// Parse the operator tail after a `(a, b, …)` row constructor
22501    /// and expand at parse time. `=` is the conjunction of element
22502    /// equalities; `<>` its negation; the order operators expand
22503    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22504    /// equalities. Anything else (a bare row value, a subquery
22505    /// RHS) errors honestly — SPG has no composite runtime value.
22506    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22507        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22508            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22509                lhs: Box::new(l.clone()),
22510                op: BinOp::Eq,
22511                rhs: Box::new(r.clone()),
22512            });
22513            let first = it.next().expect("row has at least two elements");
22514            it.fold(first, |acc, e| Expr::Binary {
22515                lhs: Box::new(acc),
22516                op: BinOp::And,
22517                rhs: Box::new(e),
22518            })
22519        }
22520        // Lexicographic (a,b) OP (c,d):
22521        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22522        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22523            if lhs.len() == 1 {
22524                return Expr::Binary {
22525                    lhs: Box::new(lhs[0].clone()),
22526                    op: last,
22527                    rhs: Box::new(rhs[0].clone()),
22528                };
22529            }
22530            let head_strict = Expr::Binary {
22531                lhs: Box::new(lhs[0].clone()),
22532                op: strict,
22533                rhs: Box::new(rhs[0].clone()),
22534            };
22535            let head_eq = Expr::Binary {
22536                lhs: Box::new(lhs[0].clone()),
22537                op: BinOp::Eq,
22538                rhs: Box::new(rhs[0].clone()),
22539            };
22540            Expr::Binary {
22541                lhs: Box::new(head_strict),
22542                op: BinOp::Or,
22543                rhs: Box::new(Expr::Binary {
22544                    lhs: Box::new(head_eq),
22545                    op: BinOp::And,
22546                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22547                }),
22548            }
22549        }
22550        let negated_in = if matches!(self.peek(), Token::Not)
22551            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22552        {
22553            self.advance();
22554            true
22555        } else {
22556            false
22557        };
22558        if matches!(self.peek(), Token::In) {
22559            self.advance();
22560            if !matches!(self.peek(), Token::LParen) {
22561                return Err(self.err(alloc::format!(
22562                    "expected '(' after row IN, got {:?}",
22563                    self.peek()
22564                )));
22565            }
22566            self.advance();
22567            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22568            // not a list of literal rows. Row-vs-list decomposes to
22569            // OR-of-AND above, but the subquery's rows are only known at
22570            // runtime, so keep it as a RowInSubquery node.
22571            if matches!(self.peek(), Token::Select) {
22572                let inner = self.parse_select_stmt()?;
22573                if !matches!(self.peek(), Token::RParen) {
22574                    return Err(self.err(alloc::format!(
22575                        "expected ')' after row IN-subquery, got {:?}",
22576                        self.peek()
22577                    )));
22578                }
22579                self.advance();
22580                let Statement::Select(s) = inner else {
22581                    unreachable!("parse_select_stmt always returns Statement::Select")
22582                };
22583                return Ok(Expr::RowInSubquery {
22584                    row,
22585                    subquery: Box::new(s),
22586                    negated: negated_in,
22587                });
22588            }
22589            let mut alternatives: Vec<Expr> = Vec::new();
22590            loop {
22591                // Optional ROW keyword before the paren row.
22592                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22593                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22594                {
22595                    self.advance();
22596                }
22597                if !matches!(self.peek(), Token::LParen) {
22598                    return Err(self.err(alloc::format!(
22599                        "expected '(' to open a row inside IN, got {:?}",
22600                        self.peek()
22601                    )));
22602                }
22603                self.advance();
22604                let mut rhs = alloc::vec![self.parse_expr(0)?];
22605                while matches!(self.peek(), Token::Comma) {
22606                    self.advance();
22607                    rhs.push(self.parse_expr(0)?);
22608                }
22609                if !matches!(self.peek(), Token::RParen) {
22610                    return Err(self.err(alloc::format!(
22611                        "expected ')' after row inside IN, got {:?}",
22612                        self.peek()
22613                    )));
22614                }
22615                self.advance();
22616                if rhs.len() != row.len() {
22617                    return Err(self.err(alloc::format!(
22618                        "row IN arity mismatch: left has {}, right has {}",
22619                        row.len(),
22620                        rhs.len()
22621                    )));
22622                }
22623                alternatives.push(row_eq(&row, &rhs));
22624                if matches!(self.peek(), Token::Comma) {
22625                    self.advance();
22626                    continue;
22627                }
22628                break;
22629            }
22630            if !matches!(self.peek(), Token::RParen) {
22631                return Err(self.err(alloc::format!(
22632                    "expected ')' to close row IN list, got {:?}",
22633                    self.peek()
22634                )));
22635            }
22636            self.advance();
22637            let mut it = alternatives.into_iter();
22638            let first = it.next().expect("IN list has at least one row");
22639            let combined = it.fold(first, |acc, e| Expr::Binary {
22640                lhs: Box::new(acc),
22641                op: BinOp::Or,
22642                rhs: Box::new(e),
22643            });
22644            return Ok(maybe_not(combined, negated_in));
22645        }
22646        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22647        // two periods share at least one time point. Each pair is
22648        // normalised with least/greatest (PG accepts the endpoints
22649        // in either order), then lowered to the standard
22650        // `start1 < end2 AND start2 < end1` form.
22651        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22652            if row.len() != 2 {
22653                return Err(self.err(alloc::format!(
22654                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22655                    row.len()
22656                )));
22657            }
22658            self.advance();
22659            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22660                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22661            {
22662                self.advance();
22663            }
22664            if !matches!(self.peek(), Token::LParen) {
22665                return Err(self.err(alloc::format!(
22666                    "expected '(' after OVERLAPS, got {:?}",
22667                    self.peek()
22668                )));
22669            }
22670            self.advance();
22671            let r0 = self.parse_expr(0)?;
22672            if !matches!(self.peek(), Token::Comma) {
22673                return Err(self.err(alloc::format!(
22674                    "OVERLAPS needs (start, end) on the right, got {:?}",
22675                    self.peek()
22676                )));
22677            }
22678            self.advance();
22679            let r1 = self.parse_expr(0)?;
22680            if !matches!(self.peek(), Token::RParen) {
22681                return Err(self.err(alloc::format!(
22682                    "expected ')' after OVERLAPS pair, got {:?}",
22683                    self.peek()
22684                )));
22685            }
22686            self.advance();
22687            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22688                name: String::from(name),
22689                args: alloc::vec![a.clone(), b.clone()],
22690            };
22691            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22692                lhs: Box::new(lhs),
22693                op: BinOp::Lt,
22694                rhs: Box::new(rhs),
22695            };
22696            return Ok(Expr::Binary {
22697                lhs: Box::new(lt(
22698                    pair_fn("least", &row[0], &row[1]),
22699                    pair_fn("greatest", &r0, &r1),
22700                )),
22701                op: BinOp::And,
22702                rhs: Box::new(lt(
22703                    pair_fn("least", &r0, &r1),
22704                    pair_fn("greatest", &row[0], &row[1]),
22705                )),
22706            });
22707        }
22708        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22709        // PG, `IS NULL` is true only when EVERY field is NULL, and
22710        // `IS NOT NULL` is true only when every field is non-NULL — the
22711        // latter is NOT the negation of the former (a mixed row is
22712        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22713        // which reproduces exactly that all-fields semantics.
22714        if matches!(self.peek(), Token::Is) {
22715            self.advance();
22716            let negated = if matches!(self.peek(), Token::Not) {
22717                self.advance();
22718                true
22719            } else {
22720                false
22721            };
22722            if !matches!(self.peek(), Token::Null) {
22723                return Err(self.err(alloc::format!(
22724                    "expected NULL after row IS [NOT], got {:?}",
22725                    self.peek()
22726                )));
22727            }
22728            self.advance();
22729            let mut it = row.iter().map(|e| Expr::IsNull {
22730                expr: Box::new(e.clone()),
22731                negated,
22732            });
22733            let first = it.next().expect("row has at least two elements");
22734            return Ok(it.fold(first, |acc, e| Expr::Binary {
22735                lhs: Box::new(acc),
22736                op: BinOp::And,
22737                rhs: Box::new(e),
22738            }));
22739        }
22740        let op = match self.peek() {
22741            Token::Eq => BinOp::Eq,
22742            Token::NotEq => BinOp::NotEq,
22743            Token::Lt => BinOp::Lt,
22744            Token::LtEq => BinOp::LtEq,
22745            Token::Gt => BinOp::Gt,
22746            Token::GtEq => BinOp::GtEq,
22747            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22748            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22749            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22750            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22751            // (`::text`, `.field`) applies at the caller just as it does for the
22752            // ROW(...) node. All the comparison / predicate forms returned above.
22753            _ => {
22754                return Ok(Expr::FunctionCall {
22755                    name: String::from("row"),
22756                    args: row,
22757                });
22758            }
22759        };
22760        self.advance();
22761        // Optional ROW keyword before the paren row.
22762        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22763            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22764        {
22765            self.advance();
22766        }
22767        if !matches!(self.peek(), Token::LParen) {
22768            return Err(self.err(alloc::format!(
22769                "expected '(' to open the right-hand row, got {:?}",
22770                self.peek()
22771            )));
22772        }
22773        self.advance();
22774        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22775        // subquery. Kept as a node (the subquery's row is a runtime value);
22776        // the literal-RHS form below still decomposes at parse time.
22777        if matches!(self.peek(), Token::Select) {
22778            let inner = self.parse_select_stmt()?;
22779            if !matches!(self.peek(), Token::RParen) {
22780                return Err(self.err(alloc::format!(
22781                    "expected ')' after row comparison subquery, got {:?}",
22782                    self.peek()
22783                )));
22784            }
22785            self.advance();
22786            let Statement::Select(s) = inner else {
22787                unreachable!("parse_select_stmt always returns Statement::Select")
22788            };
22789            return Ok(Expr::RowCmpSubquery {
22790                row,
22791                op,
22792                subquery: Box::new(s),
22793            });
22794        }
22795        let mut rhs = alloc::vec![self.parse_expr(0)?];
22796        while matches!(self.peek(), Token::Comma) {
22797            self.advance();
22798            rhs.push(self.parse_expr(0)?);
22799        }
22800        if !matches!(self.peek(), Token::RParen) {
22801            return Err(self.err(alloc::format!(
22802                "expected ')' after right-hand row, got {:?}",
22803                self.peek()
22804            )));
22805        }
22806        self.advance();
22807        if rhs.len() != row.len() {
22808            // v7.39 (round 239) — PG's wording (42601).
22809            return Err(self.err("unequal number of entries in row expressions".to_string()));
22810        }
22811        Ok(match op {
22812            BinOp::Eq => row_eq(&row, &rhs),
22813            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22814            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22815            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22816            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22817            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22818            _ => unreachable!("op restricted above"),
22819        })
22820    }
22821
22822    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22823    /// escape character becomes the matcher's default backslash:
22824    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22825    /// → the char itself, and any pre-existing backslash escapes
22826    /// itself so it stays literal. Both operands must be string
22827    /// literals — a runtime pattern would need matcher support.
22828    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22829        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22830            (&pattern, &esc)
22831        else {
22832            return Err(
22833                "LIKE ... ESCAPE requires string-literal pattern and escape \
22834                 (runtime escape characters are not supported yet)"
22835                    .into(),
22836            );
22837        };
22838        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22839        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22840        // multi-character escape is an error.
22841        let esc_ch: Option<char> = {
22842            let mut ch_iter = e.chars();
22843            match (ch_iter.next(), ch_iter.next()) {
22844                (Some(c), None) => Some(c),
22845                (None, _) => None,
22846                (Some(_), Some(_)) => {
22847                    return Err(alloc::format!(
22848                        "ESCAPE must be a single character, got {e:?}"
22849                    ));
22850                }
22851            }
22852        };
22853        let mut out = String::with_capacity(p.len() + 4);
22854        let mut chars = p.chars();
22855        while let Some(c) = chars.next() {
22856            if Some(c) == esc_ch {
22857                match chars.next() {
22858                    // Escaped wildcard / escaped escape → keep the
22859                    // next char literal via backslash.
22860                    Some(next) => {
22861                        out.push('\\');
22862                        out.push(next);
22863                    }
22864                    None => {
22865                        return Err("LIKE pattern ends with the escape character".into());
22866                    }
22867                }
22868            } else if c == '\\' && esc_ch != Some('\\') {
22869                // A raw backslash is literal under a custom (or absent) escape
22870                // — escape it for the backslash-based matcher.
22871                out.push('\\');
22872                out.push('\\');
22873            } else {
22874                out.push(c);
22875            }
22876        }
22877        Ok(Expr::Literal(Literal::String(out)))
22878    }
22879
22880    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22881    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22882    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22883    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22884    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22885    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22886    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22887    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22888    /// array expression errors honestly rather than silently mismatching.
22889    fn try_like_any_all(
22890        &mut self,
22891        base: &Expr,
22892        negated: bool,
22893        case_insensitive: bool,
22894    ) -> Result<Option<Expr>, ParseError> {
22895        let is_any = match self.peek() {
22896            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22897            Token::Ident(s)
22898                if s.eq_ignore_ascii_case("any")
22899                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22900            {
22901                true
22902            }
22903            _ => return Ok(None),
22904        };
22905        self.advance(); // ANY / ALL
22906        self.advance(); // '('
22907        let arr = self.parse_expr(0)?;
22908        if !matches!(self.peek(), Token::RParen) {
22909            return Err(self.err(format!(
22910                "expected ')' after LIKE {} argument, got {:?}",
22911                if is_any { "ANY" } else { "ALL" },
22912                self.peek()
22913            )));
22914        }
22915        self.advance(); // ')'
22916        let Expr::Array(items) = arr else {
22917            return Err(self.err(
22918                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22919            ));
22920        };
22921        let mut clauses = items.into_iter().map(|p| Expr::Like {
22922            expr: Box::new(base.clone()),
22923            pattern: Box::new(p),
22924            negated,
22925            case_insensitive,
22926        });
22927        let Some(first) = clauses.next() else {
22928            // ANY(empty) = FALSE, ALL(empty) = TRUE.
22929            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22930        };
22931        let op = if is_any { BinOp::Or } else { BinOp::And };
22932        let combined = clauses.fold(first, |acc, c| Expr::Binary {
22933            lhs: Box::new(acc),
22934            op,
22935            rhs: Box::new(c),
22936        });
22937        Ok(Some(combined))
22938    }
22939
22940    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
22941    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22942    /// `AND` is not swallowed.
22943    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22944        self.advance(); // BETWEEN
22945        // SYMMETRIC — the bounds may arrive in either order; both
22946        // orientations OR together. ASYMMETRIC is the default and
22947        // absorbs as noise.
22948        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22949        {
22950            self.advance();
22951            true
22952        } else {
22953            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22954                self.advance();
22955            }
22956            false
22957        };
22958        let low = self.parse_expr(6)?;
22959        if !matches!(self.peek(), Token::And) {
22960            return Err(self.err(format!(
22961                "expected AND after BETWEEN low bound, got {:?}",
22962                self.peek()
22963            )));
22964        }
22965        self.advance();
22966        let high = self.parse_expr(6)?;
22967        let target = Box::new(expr);
22968        let range = |lo: Expr, hi: Expr| Expr::Binary {
22969            lhs: Box::new(Expr::Binary {
22970                lhs: target.clone(),
22971                op: BinOp::GtEq,
22972                rhs: Box::new(lo),
22973            }),
22974            op: BinOp::And,
22975            rhs: Box::new(Expr::Binary {
22976                lhs: target.clone(),
22977                op: BinOp::LtEq,
22978                rhs: Box::new(hi),
22979            }),
22980        };
22981        let combined = if symmetric {
22982            Expr::Binary {
22983                lhs: Box::new(range(low.clone(), high.clone())),
22984                op: BinOp::Or,
22985                rhs: Box::new(range(high, low)),
22986            }
22987        } else {
22988            range(low, high)
22989        };
22990        Ok(maybe_not(combined, negated))
22991    }
22992
22993    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
22994    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
22995    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
22996    /// Caller already consumed the leading `WITH` ident.
22997    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
22998    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
22999    /// self-reference that appears more than once in a single term.
23000    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23001        use crate::ast::{CteBody, SelectStatement};
23002        if !cte.recursive {
23003            return Ok(());
23004        }
23005        let CteBody::Select(body) = &cte.body else {
23006            return Ok(());
23007        };
23008        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23009        // check the anchor and every peer term.
23010        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23011        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23012        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23013            return Err(self.err(String::from(
23014                "ORDER BY in a recursive query is not implemented",
23015            )));
23016        }
23017        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23018            return Err(self.err(String::from(
23019                "LIMIT in a recursive query is not implemented",
23020            )));
23021        }
23022        let self_refs = |s: &SelectStatement| -> usize {
23023            let Some(from) = &s.from else {
23024                return 0;
23025            };
23026            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23027            for j in &from.joins {
23028                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23029                    n += 1;
23030                }
23031            }
23032            n
23033        };
23034        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23035            return Err(self.err(alloc::format!(
23036                "recursive reference to query \"{}\" must not appear more than once",
23037                cte.name
23038            )));
23039        }
23040        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23041        // apply only when the body actually references itself (a non-self-
23042        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23043        let anchor_refs = self_refs(body);
23044        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23045        if anchor_refs > 0 || union_refs {
23046            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23047            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23048            // "does not have the form" error — SPG used to compute a value.
23049            if body.unions.is_empty()
23050                || body.unions.iter().any(|(k, _)| {
23051                    !matches!(
23052                        k,
23053                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23054                    )
23055                })
23056            {
23057                return Err(self.err(alloc::format!(
23058                    "recursive query \"{}\" does not have the form non-recursive-term \
23059                     UNION [ALL] recursive-term",
23060                    cte.name
23061                )));
23062            }
23063            if anchor_refs > 0 {
23064                return Err(self.err(alloc::format!(
23065                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23066                    cte.name
23067                )));
23068            }
23069        }
23070        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23071        for (_, u) in &body.unions {
23072            if self_refs(u) == 0 {
23073                continue;
23074            }
23075            // The self-reference must not sit on the nullable side of an outer
23076            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23077            if let Some(from) = &u.from {
23078                for (i, j) in from.joins.iter().enumerate() {
23079                    let left_has_self = is_self(&from.primary)
23080                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23081                    let violated = match j.kind {
23082                        crate::ast::JoinKind::Left => is_self(&j.table),
23083                        crate::ast::JoinKind::Right => left_has_self,
23084                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23085                        _ => false,
23086                    };
23087                    if violated {
23088                        return Err(self.err(alloc::format!(
23089                            "recursive reference to query \"{}\" must not appear within an outer join",
23090                            cte.name
23091                        )));
23092                    }
23093                }
23094            }
23095            // No aggregates at the top level of the recursive term (SPG used
23096            // to run them and surface a misleading downstream error).
23097            let mut items_and_having: Vec<&Expr> = Vec::new();
23098            for it in &u.items {
23099                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23100                    items_and_having.push(expr);
23101                }
23102            }
23103            if let Some(h) = &u.having {
23104                items_and_having.push(h);
23105            }
23106            for e in items_and_having {
23107                if expr_has_toplevel_aggregate(e) {
23108                    return Err(self.err(String::from(
23109                        "aggregate functions are not allowed in a recursive query's recursive term",
23110                    )));
23111                }
23112            }
23113        }
23114        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23115        // subquery) anywhere in the body is rejected; a plain FROM derived
23116        // table is legal in PG and untouched here.
23117        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23118        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23119        for term in all_terms {
23120            if select_has_self_ref_in_sublink(term, &cte.name) {
23121                return Err(self.err(alloc::format!(
23122                    "recursive reference to query \"{}\" must not appear within a subquery",
23123                    cte.name
23124                )));
23125            }
23126        }
23127        Ok(())
23128    }
23129
23130    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23131    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23132    /// right after parse so the engine sees a plain recursive CTE with the
23133    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23134    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23135    /// text-rendered rows can't provide, and errors honestly.
23136    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23137        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23138        if cte.search.is_none() && cte.cycle.is_none() {
23139            return Ok(());
23140        }
23141        let cte_name = cte.name.clone();
23142        let col_names = cte.column_overrides.clone();
23143        if col_names.is_empty() {
23144            return Err(
23145                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23146            );
23147        }
23148        let search = cte.search.take();
23149        let cycle = cte.cycle.take();
23150        let mut extra_cols: Vec<String> = Vec::new();
23151        let col_ref = |name: &str| {
23152            Expr::Column(ColumnName {
23153                qualifier: Some(cte_name.clone()),
23154                name: name.to_string(),
23155            })
23156        };
23157        // Position of a SEARCH/CYCLE column within the CTE's column list.
23158        let pos_of = |name: &str| -> Result<usize, ParseError> {
23159            col_names
23160                .iter()
23161                .position(|c| c.eq_ignore_ascii_case(name))
23162                .ok_or_else(|| {
23163                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23164                })
23165        };
23166        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23167            let mut args = Vec::with_capacity(positions.len());
23168            for &p in positions {
23169                match items.get(p) {
23170                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23171                    _ => {
23172                        return Err(self.err(
23173                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23174                        ));
23175                    }
23176                }
23177            }
23178            Ok(Expr::FunctionCall {
23179                name: "row".into(),
23180                args,
23181            })
23182        };
23183        let CteBody::Select(body) = &mut cte.body else {
23184            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23185        };
23186        if body.unions.is_empty() {
23187            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23188        }
23189        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23190
23191        if let Some(srch) = search {
23192            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23193            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23194            // no typed `record[]`, but element-wise array ORDER BY is correct
23195            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23196            // exactly onto a typed array: DEPTH is the root→node path
23197            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23198            // orders numerically (multi-digit keys included), matching PG.
23199            //
23200            // A multi-column BY would need a record[] to keep the per-node key
23201            // tuple orderable, which SPG can't express — error honestly there
23202            // rather than mis-order.
23203            if srch.by_columns.len() != 1 {
23204                return Err(self.err(
23205                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23206                     SPG doesn't have yet; a single BY column is supported"
23207                        .into(),
23208                ));
23209            }
23210            let key_pos = pos_of(&srch.by_columns[0])?;
23211            let base_key = match body.items.get(key_pos) {
23212                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23213                _ => {
23214                    return Err(
23215                        self.err("SEARCH BY column maps to a non-expression select item".into())
23216                    );
23217                }
23218            };
23219            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23220                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23221                _ => {
23222                    return Err(
23223                        self.err("SEARCH BY column maps to a non-expression select item".into())
23224                    );
23225                }
23226            };
23227            if srch.depth_first {
23228                // base: ARRAY[key]; rec: array_append(cte.set, key).
23229                body.items.push(SelectItem::Expr {
23230                    expr: Expr::Array(alloc::vec![base_key]),
23231                    alias: Some(srch.set_column.clone()),
23232                });
23233                body.unions[rec].1.items.push(SelectItem::Expr {
23234                    expr: Expr::FunctionCall {
23235                        name: "array_append".into(),
23236                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23237                    },
23238                    alias: Some(srch.set_column.clone()),
23239                });
23240            } else {
23241                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23242                // leading depth element dominates the element-wise comparison,
23243                // so shallower rows sort first, then by key — PG's (depth, key).
23244                body.items.push(SelectItem::Expr {
23245                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23246                    alias: Some(srch.set_column.clone()),
23247                });
23248                // rec depth = cte.set[1] + 1.
23249                let parent_depth = Expr::ArraySubscript {
23250                    target: Box::new(col_ref(&srch.set_column)),
23251                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23252                };
23253                body.unions[rec].1.items.push(SelectItem::Expr {
23254                    expr: Expr::Array(alloc::vec![
23255                        Expr::Binary {
23256                            lhs: Box::new(parent_depth),
23257                            op: BinOp::Add,
23258                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23259                        },
23260                        rec_key,
23261                    ]),
23262                    alias: Some(srch.set_column.clone()),
23263                });
23264            }
23265            extra_cols.push(srch.set_column);
23266        }
23267
23268        if let Some(cyc) = cycle {
23269            let positions: Vec<usize> = cyc
23270                .columns
23271                .iter()
23272                .map(|c| pos_of(c))
23273                .collect::<Result<_, _>>()?;
23274            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23275            // cast it to text for the cycle path: membership only needs equality,
23276            // and the record text form gives SPG a TextArray path (SPG has no
23277            // typed record[] array). Cycle detection is unaffected.
23278            let base_row = Expr::Cast {
23279                expr: Box::new(row_of(&body.items, &positions)?),
23280                target: CastTarget::Text,
23281            };
23282            let rec_row = Expr::Cast {
23283                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23284                target: CastTarget::Text,
23285            };
23286            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23287            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23288            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23289            body.items.push(SelectItem::Expr {
23290                expr: Expr::Literal(dflt.clone()),
23291                alias: Some(cyc.mark_column.clone()),
23292            });
23293            body.items.push(SelectItem::Expr {
23294                expr: Expr::Array(alloc::vec![base_row]),
23295                alias: Some(cyc.path_column.clone()),
23296            });
23297            // rec mark: ROW(cols) already in the path → cycle.
23298            let hit = Expr::AnyAll {
23299                expr: Box::new(rec_row.clone()),
23300                op: BinOp::Eq,
23301                array: Box::new(col_ref(&cyc.path_column)),
23302                is_any: true,
23303            };
23304            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23305                Expr::Case {
23306                    operand: None,
23307                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23308                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23309                }
23310            } else {
23311                hit
23312            };
23313            body.unions[rec].1.items.push(SelectItem::Expr {
23314                expr: mark_expr,
23315                alias: Some(cyc.mark_column.clone()),
23316            });
23317            // rec path: array_append(cte.path, ROW(cols)).
23318            body.unions[rec].1.items.push(SelectItem::Expr {
23319                expr: Expr::FunctionCall {
23320                    name: "array_append".into(),
23321                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23322                },
23323                alias: Some(cyc.path_column.clone()),
23324            });
23325            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23326            let stop = Expr::Unary {
23327                op: UnOp::Not,
23328                expr: Box::new(col_ref(&cyc.mark_column)),
23329            };
23330            let w = &mut body.unions[rec].1.where_;
23331            *w = Some(match w.take() {
23332                Some(prev) => Expr::Binary {
23333                    lhs: Box::new(prev),
23334                    op: BinOp::And,
23335                    rhs: Box::new(stop),
23336                },
23337                None => stop,
23338            });
23339            extra_cols.push(cyc.mark_column);
23340            extra_cols.push(cyc.path_column);
23341        }
23342        cte.column_overrides.extend(extra_cols);
23343        Ok(())
23344    }
23345
23346    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23347    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23348    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23349        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23350            return Ok(None);
23351        }
23352        self.advance(); // SEARCH
23353        let depth_first = match self.peek() {
23354            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23355            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23356            other => {
23357                return Err(self.err(format!(
23358                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23359                )));
23360            }
23361        };
23362        self.advance();
23363        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23364            return Err(self.err(format!(
23365                "expected FIRST after SEARCH mode, got {:?}",
23366                self.peek()
23367            )));
23368        }
23369        self.advance();
23370        if !self.peek_is_by() {
23371            return Err(self.err(format!(
23372                "expected BY after SEARCH … FIRST, got {:?}",
23373                self.peek()
23374            )));
23375        }
23376        self.advance();
23377        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23378        while matches!(self.peek(), Token::Comma) {
23379            self.advance();
23380            by_columns.push(self.expect_ident_like()?);
23381        }
23382        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23383            return Err(self.err(format!(
23384                "expected SET in SEARCH clause, got {:?}",
23385                self.peek()
23386            )));
23387        }
23388        self.advance();
23389        let set_column = self.expect_ident_like()?;
23390        Ok(Some(crate::ast::SearchClause {
23391            depth_first,
23392            by_columns,
23393            set_column,
23394        }))
23395    }
23396
23397    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23398    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23399    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23400        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23401            return Ok(None);
23402        }
23403        self.advance(); // CYCLE
23404        let mut columns = alloc::vec![self.expect_ident_like()?];
23405        while matches!(self.peek(), Token::Comma) {
23406            self.advance();
23407            columns.push(self.expect_ident_like()?);
23408        }
23409        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23410            return Err(self.err(format!(
23411                "expected SET in CYCLE clause, got {:?}",
23412                self.peek()
23413            )));
23414        }
23415        self.advance();
23416        let mark_column = self.expect_ident_like()?;
23417        let mut mark_value = None;
23418        let mut default_value = None;
23419        if matches!(self.peek(), Token::To) {
23420            self.advance();
23421            mark_value = Some(self.parse_cycle_literal()?);
23422            if !matches!(self.peek(), Token::Default) {
23423                return Err(self.err(format!(
23424                    "expected DEFAULT after CYCLE … TO, got {:?}",
23425                    self.peek()
23426                )));
23427            }
23428            self.advance();
23429            default_value = Some(self.parse_cycle_literal()?);
23430        }
23431        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23432            return Err(self.err(format!(
23433                "expected USING in CYCLE clause, got {:?}",
23434                self.peek()
23435            )));
23436        }
23437        self.advance();
23438        let path_column = self.expect_ident_like()?;
23439        Ok(Some(crate::ast::CycleClause {
23440            columns,
23441            mark_column,
23442            mark_value,
23443            default_value,
23444            path_column,
23445        }))
23446    }
23447
23448    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23449    /// literal (string / bool / number) in PG.
23450    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23451        match self.parse_expr(0)? {
23452            Expr::Literal(l) => Ok(l),
23453            other => Err(self.err(format!(
23454                "CYCLE mark/default value must be a literal, got {other:?}"
23455            ))),
23456        }
23457    }
23458
23459    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23460        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23461        // Comes through as an identifier; consume it if present and
23462        // mark every CTE in the clause as recursive (PG semantics —
23463        // the flag is per-WITH, not per-CTE).
23464        let mut recursive = false;
23465        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23466            && s.eq_ignore_ascii_case("recursive")
23467        {
23468            self.advance();
23469            recursive = true;
23470        }
23471        let mut ctes = Vec::new();
23472        loop {
23473            let name = self.expect_ident_like()?;
23474            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23475            // PG uses these to rename the body's output columns; we
23476            // do the same below by overriding `columns[i].name`.
23477            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23478                self.advance();
23479                let mut names = Vec::new();
23480                loop {
23481                    names.push(self.expect_ident_like()?);
23482                    if matches!(self.peek(), Token::Comma) {
23483                        self.advance();
23484                        continue;
23485                    }
23486                    break;
23487                }
23488                if !matches!(self.peek(), Token::RParen) {
23489                    return Err(self.err(format!(
23490                        "expected ')' to close CTE column list, got {:?}",
23491                        self.peek()
23492                    )));
23493                }
23494                self.advance();
23495                names
23496            } else {
23497                Vec::new()
23498            };
23499            // AS is a reserved Token::As (used by SELECT-item / FROM
23500            // aliasing) — handle it specially rather than as a bare
23501            // ident.
23502            if !matches!(self.peek(), Token::As) {
23503                return Err(self.err(format!(
23504                    "expected AS after CTE name {name:?}, got {:?}",
23505                    self.peek()
23506                )));
23507            }
23508            self.advance();
23509            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23510            // MATERIALIZED` optimizer hints. SPG materialises every
23511            // CTE, so both spellings are accepted and absorbed.
23512            if matches!(self.peek(), Token::Not) {
23513                self.advance(); // NOT
23514                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23515                    if s.eq_ignore_ascii_case("materialized"))
23516                {
23517                    self.advance();
23518                } else {
23519                    return Err(self.err(format!(
23520                        "expected MATERIALIZED after AS NOT, got {:?}",
23521                        self.peek()
23522                    )));
23523                }
23524            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23525                if s.eq_ignore_ascii_case("materialized"))
23526            {
23527                self.advance();
23528            }
23529            if !matches!(self.peek(), Token::LParen) {
23530                return Err(self.err(format!(
23531                    "expected '(' after AS in WITH clause, got {:?}",
23532                    self.peek()
23533                )));
23534            }
23535            self.advance();
23536            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23537            // RETURNING) as the CTE body in addition to SELECT.
23538            // PG writable CTE semantics. UPDATE / DELETE come in as
23539            // bare Idents (lexer keeps SELECT / INSERT as reserved
23540            // tokens but treats the rest of DML as case-insensitive
23541            // idents).
23542            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23543            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23544            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23545            let body = match self.peek() {
23546                Token::Select => {
23547                    let inner = self.parse_select_stmt()?;
23548                    let Statement::Select(s) = inner else {
23549                        unreachable!("parse_select_stmt returns Select");
23550                    };
23551                    crate::ast::CteBody::Select(s)
23552                }
23553                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23554                // `SELECT * FROM t` this way and accepts it wherever a
23555                // SELECT goes, so the CTE body dispatch needs its own
23556                // arm: this match is keyed on the FIRST token, and
23557                // `Token::Table` fell through to a tail that then
23558                // rejected what it got. `parse_table_shorthand` has
23559                // returned a desugared SelectStatement since the
23560                // shorthand landed — only the routing was missing.
23561                // Round 868 found this by putting the shorthand in a
23562                // subquery; every earlier check used a top-level form.
23563                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23564                // `SELECT * FROM t` this way and accepts it wherever a
23565                // SELECT goes, so the CTE body dispatch needs its own
23566                // arm: this match is keyed on the FIRST token, and
23567                // `Token::Table` fell through to a tail that rejected
23568                // what it got. `parse_table_shorthand` has returned a
23569                // desugared SelectStatement since the shorthand landed —
23570                // only the routing was missing, here and in the derived
23571                // table's second-token gate. Round 868 found both by
23572                // putting the shorthand in a subquery; every earlier
23573                // check had used a top-level form.
23574                Token::Table
23575                    if matches!(
23576                        self.tokens.get(self.pos + 1),
23577                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23578                    ) =>
23579                {
23580                    let mut head = self.parse_table_shorthand()?;
23581                    self.parse_setop_chain_into(&mut head)?;
23582                    self.parse_select_tail_into(&mut head)?;
23583                    crate::ast::CteBody::Select(head)
23584                }
23585                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23586                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23587                // the shared rows helper onto a Select body.
23588                Token::Values => {
23589                    self.advance(); // VALUES
23590                    let mut head = self.parse_values_rows_body()?;
23591                    // A VALUES seed can head a set-operation chain —
23592                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23593                    // SELECT n+1 FROM t …). Attach any trailing
23594                    // UNION / INTERSECT / EXCEPT peers so the
23595                    // recursive-CTE body parses like the SELECT seed.
23596                    self.parse_setop_chain_into(&mut head)?;
23597                    crate::ast::CteBody::Select(head)
23598                }
23599                Token::Insert => {
23600                    let inner = self.parse_one_statement()?;
23601                    let Statement::Insert(s) = inner else {
23602                        unreachable!("Token::Insert routes to Insert");
23603                    };
23604                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23605                }
23606                _ if is_update_kw => {
23607                    let inner = self.parse_one_statement()?;
23608                    let Statement::Update(s) = inner else {
23609                        return Err(
23610                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23611                        );
23612                    };
23613                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23614                }
23615                _ if is_delete_kw => {
23616                    let inner = self.parse_one_statement()?;
23617                    let Statement::Delete(s) = inner else {
23618                        return Err(
23619                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23620                        );
23621                    };
23622                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23623                }
23624                // v7.39 (round 149) — PG 17 allows MERGE as a
23625                // data-modifying CTE body.
23626                _ if is_merge_kw => {
23627                    let inner = self.parse_one_statement()?;
23628                    let Statement::Merge(s) = inner else {
23629                        return Err(
23630                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23631                        );
23632                    };
23633                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23634                }
23635                // v7.39 (round 151) — a CTE body may itself be
23636                // WITH-headed (PG grammar: PreparableStmt carries its
23637                // own with_clause). The nested statement keeps its own
23638                // ctes; the modifying-CTE-at-top-level rule is enforced
23639                // at execution.
23640                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23641                    self.advance(); // WITH
23642                    match self.parse_with_cte_then_select()? {
23643                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23644                        Statement::Insert(s) => {
23645                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23646                        }
23647                        Statement::Update(s) => {
23648                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23649                        }
23650                        Statement::Delete(s) => {
23651                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23652                        }
23653                        Statement::Merge(s) => {
23654                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23655                        }
23656
23657                        other => {
23658                            return Err(self.err(format!(
23659                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23660                            )));
23661                        }
23662                    }
23663                }
23664                other => {
23665                    return Err(self.err(format!(
23666                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23667                    )));
23668                }
23669            };
23670            if !matches!(self.peek(), Token::RParen) {
23671                return Err(self.err(format!(
23672                    "expected ')' after CTE body, got {:?}",
23673                    self.peek()
23674                )));
23675            }
23676            self.advance();
23677            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23678            // CTE, desugared into extra body columns by the engine.
23679            let search = self.parse_cte_search_clause()?;
23680            let cycle = self.parse_cte_cycle_clause()?;
23681            let mut cte = crate::ast::Cte {
23682                name,
23683                body,
23684                recursive,
23685                column_overrides,
23686                search,
23687                cycle,
23688            };
23689            self.validate_recursive_cte(&cte)?;
23690            self.desugar_cte_search_cycle(&mut cte)?;
23691            ctes.push(cte);
23692            if matches!(self.peek(), Token::Comma) {
23693                self.advance();
23694                continue;
23695            }
23696            break;
23697        }
23698        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23699        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23700        // the parsed CTEs to whichever statement the body produces.
23701        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23702        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23703        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23704        match self.peek() {
23705            Token::Select => {
23706                let body_stmt = self.parse_select_stmt()?;
23707                let Statement::Select(mut body) = body_stmt else {
23708                    unreachable!()
23709                };
23710                body.ctes = ctes;
23711                Ok(Statement::Select(body))
23712            }
23713            Token::Insert => {
23714                let body_stmt = self.parse_one_statement()?;
23715                let Statement::Insert(mut body) = body_stmt else {
23716                    unreachable!()
23717                };
23718                body.ctes = ctes;
23719                Ok(Statement::Insert(body))
23720            }
23721            _ if outer_is_update => {
23722                let body_stmt = self.parse_one_statement()?;
23723                let Statement::Update(mut body) = body_stmt else {
23724                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23725                };
23726                body.ctes = ctes;
23727                Ok(Statement::Update(body))
23728            }
23729            _ if outer_is_delete => {
23730                let body_stmt = self.parse_one_statement()?;
23731                let Statement::Delete(mut body) = body_stmt else {
23732                    return Err(self.err(format!("expected DELETE after WITH clause")));
23733                };
23734                body.ctes = ctes;
23735                Ok(Statement::Delete(body))
23736            }
23737            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23738            // WITH RECURSIVE is rejected with PG's exact message
23739            // (parse analysis, transformWithClause).
23740            _ if outer_is_merge => {
23741                if recursive {
23742                    return Err(self.err(String::from(
23743                        "WITH RECURSIVE is not supported for MERGE statement",
23744                    )));
23745                }
23746                let body_stmt = self.parse_one_statement()?;
23747                let Statement::Merge(mut body) = body_stmt else {
23748                    return Err(self.err(format!("expected MERGE after WITH clause")));
23749                };
23750                body.ctes = ctes;
23751                Ok(Statement::Merge(body))
23752            }
23753            other => Err(self.err(format!(
23754                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23755            ))),
23756        }
23757    }
23758
23759    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23760    /// already consumed the leading `EXISTS` ident via
23761    /// `self.advance()`.
23762    /// v7.13.0 — parse the rest of a `CASE … END` expression after
23763    /// the leading `CASE` ident has been consumed (mailrs round-5
23764    /// G9). Supports both the searched form
23765    /// (`CASE WHEN cond THEN val …`) and the simple form
23766    /// (`CASE operand WHEN val THEN val …`).
23767    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23768        // Disambiguate searched vs simple form: if the next token
23769        // is `WHEN`, we're in the searched form. Otherwise the
23770        // intervening expression is the operand.
23771        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23772            None
23773        } else {
23774            Some(Box::new(self.parse_expr(0)?))
23775        };
23776        let mut branches: Vec<(Expr, Expr)> = Vec::new();
23777        loop {
23778            match self.peek() {
23779                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23780                    self.advance();
23781                    let cond = self.parse_expr(0)?;
23782                    match self.peek() {
23783                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23784                            self.advance();
23785                        }
23786                        other => {
23787                            return Err(self.err(alloc::format!(
23788                                "expected THEN after CASE WHEN <expr>, got {other:?}"
23789                            )));
23790                        }
23791                    }
23792                    let value = self.parse_expr(0)?;
23793                    branches.push((cond, value));
23794                }
23795                _ => break,
23796            }
23797        }
23798        if branches.is_empty() {
23799            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23800        }
23801        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23802        {
23803            self.advance();
23804            Some(Box::new(self.parse_expr(0)?))
23805        } else {
23806            None
23807        };
23808        match self.peek() {
23809            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23810                self.advance();
23811            }
23812            other => {
23813                return Err(self.err(alloc::format!(
23814                    "expected END to close CASE expression, got {other:?}"
23815                )));
23816            }
23817        }
23818        Ok(Expr::Case {
23819            operand,
23820            branches,
23821            else_branch,
23822        })
23823    }
23824
23825    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23826    /// query-source position (EXISTS / IN / INSERT source / CTE body /
23827    /// view body). Caller consumed the WITH keyword. Only a SELECT
23828    /// outer is grammatical here; the data-modifying-CTE-at-top-level
23829    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23830    /// maps correctly.
23831    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23832        let inner = self.parse_with_cte_then_select()?;
23833        match inner {
23834            Statement::Select(s) => Ok(s),
23835            other => Err(self.err(format!(
23836                "expected SELECT after WITH in a subquery, got {other:?}"
23837            ))),
23838        }
23839    }
23840
23841    /// True when the next token is the (unquoted) WITH keyword. WITH is
23842    /// reserved in PG, so a bare `with` can never be a column reference
23843    /// in these positions; a quoted `"with"` stays an identifier.
23844    fn peek_is_with_kw(&self) -> bool {
23845        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23846    }
23847
23848    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23849    /// `#[inline(never)]` keeps the large SelectStatement temporaries
23850    /// off parse_expr's recursive frame (the nesting-budget stack
23851    /// cliff — see the round-153 gate regression).
23852    #[inline(never)]
23853    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23854        if self.peek_is_with_kw() {
23855            self.advance();
23856            self.parse_nested_with_select()
23857        } else {
23858            match self.parse_select_stmt()? {
23859                Statement::Select(s) => Ok(s),
23860                other => Err(self.err(alloc::format!(
23861                    "expected SELECT inside ANY/ALL, got {other:?}"
23862                ))),
23863            }
23864        }
23865    }
23866
23867    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23868        if !matches!(self.peek(), Token::LParen) {
23869            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23870        }
23871        self.advance();
23872        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23873        let s = if self.peek_is_with_kw() {
23874            self.advance();
23875            self.parse_nested_with_select()?
23876        } else {
23877            let inner = self.parse_select_stmt()?;
23878            let Statement::Select(s) = inner else {
23879                unreachable!("parse_select_stmt returns Select")
23880            };
23881            s
23882        };
23883        if !matches!(self.peek(), Token::RParen) {
23884            return Err(self.err(format!(
23885                "expected ')' after EXISTS-subquery, got {:?}",
23886                self.peek()
23887            )));
23888        }
23889        self.advance();
23890        Ok(Expr::Exists {
23891            subquery: Box::new(s),
23892            negated,
23893        })
23894    }
23895
23896    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23897        self.advance(); // IN
23898        if !matches!(self.peek(), Token::LParen) {
23899            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23900        }
23901        self.advance();
23902        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23903        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23904        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23905            let s = if self.peek_is_with_kw() {
23906                self.advance();
23907                self.parse_nested_with_select()?
23908            } else {
23909                let inner = self.parse_select_stmt()?;
23910                let Statement::Select(s) = inner else {
23911                    unreachable!("parse_select_stmt always returns Statement::Select")
23912                };
23913                s
23914            };
23915            if !matches!(self.peek(), Token::RParen) {
23916                return Err(self.err(format!(
23917                    "expected ')' after IN-subquery, got {:?}",
23918                    self.peek()
23919                )));
23920            }
23921            self.advance();
23922            return Ok(Expr::InSubquery {
23923                expr: Box::new(expr),
23924                subquery: Box::new(s),
23925                negated,
23926            });
23927        }
23928        let mut elements = Vec::new();
23929        if !matches!(self.peek(), Token::RParen) {
23930            loop {
23931                elements.push(self.parse_expr(0)?);
23932                match self.peek() {
23933                    Token::Comma => {
23934                        self.advance();
23935                    }
23936                    Token::RParen => break,
23937                    other => {
23938                        return Err(
23939                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23940                        );
23941                    }
23942                }
23943            }
23944        }
23945        self.advance(); // ')'
23946        // v7.30.2 (mailrs round-25) — flat InList node instead of a
23947        // left-deep OR-Eq chain: chain depth scaled with the element
23948        // count and overflowed the stack (eval + drop are recursive).
23949        if elements.is_empty() {
23950            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23951        }
23952        Ok(Expr::InList {
23953            expr: Box::new(expr),
23954            list: elements,
23955            negated,
23956        })
23957    }
23958
23959    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23960    /// already consumed by the caller. Elements must be numeric literals
23961    /// (with optional unary `-`); any compound expression is rejected at
23962    /// parse time so the runtime never needs to evaluate inside a vector.
23963    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23964    /// has already consumed the `EXTRACT` token before calling us —
23965    /// we pick up at the opening `(`.
23966    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23967    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23968    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23969    /// per-column OR-fold of
23970    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23971    /// term)` so the existing FTS evaluator handles semantics.
23972    ///
23973    /// The mode modifier is accepted-and-ignored at v7.17 — all
23974    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23975    /// mode operators (`+foo -bar`) would need their own parser
23976    /// (Phase 2.2c); customers who hit them today already get a
23977    /// correct lexeme-match against the bare term, only without
23978    /// the +/- precedence the customer asked for.
23979    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23980        // Already at `MATCH`-consumed position; the dispatcher
23981        // confirmed the next token is `(`.
23982        if !matches!(self.peek(), Token::LParen) {
23983            return Err(self.err(alloc::format!(
23984                "expected '(' after MATCH, got {:?}",
23985                self.peek()
23986            )));
23987        }
23988        self.advance();
23989        let mut cols: Vec<Expr> = Vec::new();
23990        loop {
23991            cols.push(self.parse_expr(0)?);
23992            match self.peek() {
23993                Token::Comma => {
23994                    self.advance();
23995                }
23996                Token::RParen => break,
23997                other => {
23998                    return Err(self.err(alloc::format!(
23999                        "expected ',' or ')' in MATCH column list, got {other:?}"
24000                    )));
24001                }
24002            }
24003        }
24004        self.advance(); // ')'
24005        // Expect AGAINST.
24006        match self.peek() {
24007            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24008                self.advance();
24009            }
24010            other => {
24011                return Err(self.err(alloc::format!(
24012                    "expected AGAINST after MATCH column list, got {other:?}"
24013                )));
24014            }
24015        }
24016        if !matches!(self.peek(), Token::LParen) {
24017            return Err(self.err(alloc::format!(
24018                "expected '(' after AGAINST, got {:?}",
24019                self.peek()
24020            )));
24021        }
24022        self.advance();
24023        // Read AGAINST's argument as a single primary token —
24024        // string literal, placeholder, or column-ref ident. We
24025        // can't call `parse_expr` / `parse_unary` here because
24026        // the postfix chain inside `parse_atom` would greedily
24027        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24028        // and fail at "expected '(' after IN". Customers always
24029        // write a literal or bound parameter in AGAINST, so this
24030        // restriction is non-blocking; the error path explains
24031        // the limit if a more complex expression shows up.
24032        let term = match self.advance() {
24033            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24034            Token::Placeholder(n) => Expr::Placeholder(n),
24035            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24036                qualifier: None,
24037                name: s,
24038            }),
24039            other => {
24040                return Err(self.err(alloc::format!(
24041                    "MATCH ... AGAINST(<term>) expects a string literal, \
24042                     bound parameter, or column ref, got {other:?}"
24043                )));
24044            }
24045        };
24046        // Optional mode tail — accept-and-ignore at v7.17:
24047        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24048        //   IN BOOLEAN MODE
24049        //   WITH QUERY EXPANSION
24050        loop {
24051            match self.peek() {
24052                // IN lexes as a reserved Token::In, not an ident,
24053                // so it gets its own arm.
24054                Token::In => {
24055                    self.advance();
24056                }
24057                Token::Ident(s) | Token::QuotedIdent(s)
24058                    if s.eq_ignore_ascii_case("natural")
24059                        || s.eq_ignore_ascii_case("language")
24060                        || s.eq_ignore_ascii_case("boolean")
24061                        || s.eq_ignore_ascii_case("mode")
24062                        || s.eq_ignore_ascii_case("with")
24063                        || s.eq_ignore_ascii_case("query")
24064                        || s.eq_ignore_ascii_case("expansion") =>
24065                {
24066                    self.advance();
24067                }
24068                _ => break,
24069            }
24070        }
24071        if !matches!(self.peek(), Token::RParen) {
24072            return Err(self.err(alloc::format!(
24073                "expected ')' to close AGAINST, got {:?}",
24074                self.peek()
24075            )));
24076        }
24077        self.advance();
24078        // Build per-column `to_tsvector('simple', col) @@
24079        // plainto_tsquery('simple', term)` and OR-fold.
24080        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24081        let plainto = Expr::FunctionCall {
24082            name: String::from("plainto_tsquery"),
24083            args: alloc::vec![simple_lit(), term.clone()],
24084        };
24085        let mut folded: Option<Expr> = None;
24086        for col in cols {
24087            let to_tsv = Expr::FunctionCall {
24088                name: String::from("to_tsvector"),
24089                args: alloc::vec![simple_lit(), col],
24090            };
24091            let leaf = Expr::Binary {
24092                lhs: Box::new(to_tsv),
24093                op: crate::ast::BinOp::TsMatch,
24094                rhs: Box::new(plainto.clone()),
24095            };
24096            folded = Some(match folded {
24097                None => leaf,
24098                Some(prev) => Expr::Binary {
24099                    lhs: Box::new(prev),
24100                    op: crate::ast::BinOp::Or,
24101                    rhs: Box::new(leaf),
24102                },
24103            });
24104        }
24105        match folded {
24106            Some(e) => Ok(e),
24107            None => Err(self.err(String::from(
24108                "MATCH(...) AGAINST(...) requires at least one column",
24109            ))),
24110        }
24111    }
24112
24113    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24114        if !matches!(self.peek(), Token::LParen) {
24115            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24116        }
24117        self.advance();
24118        let field_name = self.expect_ident_like()?;
24119        let field = match field_name.to_ascii_lowercase().as_str() {
24120            // PG accepts the plural spellings (years/months/…/millenniums) as
24121            // aliases for the singular fields — its datetime unit table has both.
24122            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24123            "year" | "years" => ExtractField::Year,
24124            "month" | "months" => ExtractField::Month,
24125            "day" | "days" => ExtractField::Day,
24126            "hour" | "hours" => ExtractField::Hour,
24127            "minute" | "minutes" => ExtractField::Minute,
24128            "second" | "seconds" => ExtractField::Second,
24129            "microsecond" | "microseconds" => ExtractField::Microsecond,
24130            "epoch" => ExtractField::Epoch,
24131            "dow" => ExtractField::Dow,
24132            "isodow" => ExtractField::Isodow,
24133            "doy" => ExtractField::Doy,
24134            "week" | "weeks" => ExtractField::Week,
24135            "isoyear" => ExtractField::Isoyear,
24136            "quarter" => ExtractField::Quarter,
24137            "decade" | "decades" => ExtractField::Decade,
24138            "century" | "centuries" => ExtractField::Century,
24139            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24140            "julian" => ExtractField::Julian,
24141            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24142            "timezone" => ExtractField::Timezone,
24143            "timezone_hour" => ExtractField::TimezoneHour,
24144            "timezone_minute" => ExtractField::TimezoneMinute,
24145            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24146            // reports an unknown one with the source type (22023); carry the
24147            // raw name so eval can word it.
24148            other => ExtractField::Other(alloc::string::String::from(other)),
24149        };
24150        if !matches!(self.peek(), Token::From) {
24151            return Err(self.err(format!(
24152                "expected FROM after EXTRACT field, got {:?}",
24153                self.peek()
24154            )));
24155        }
24156        self.advance();
24157        let source = self.parse_expr(0)?;
24158        if !matches!(self.peek(), Token::RParen) {
24159            return Err(self.err(format!(
24160                "expected ')' to close EXTRACT, got {:?}",
24161                self.peek()
24162            )));
24163        }
24164        self.advance();
24165        Ok(Expr::Extract {
24166            field,
24167            source: Box::new(source),
24168        })
24169    }
24170
24171    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24172    /// is already consumed; we expect a single string literal next and
24173    /// resolve it into `Literal::Interval` at parse time so the engine
24174    /// never has to re-tokenise inside the string.
24175    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24176    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24177    /// is the SQL-standard form and is left to the path below.
24178    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24179        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24180        let (offset, sign) = match self.peek() {
24181            Token::Minus => (1, "-"),
24182            _ => (0, ""),
24183        };
24184        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24185            return None;
24186        };
24187        self.tokens
24188            .get(self.pos + offset + 1)
24189            .filter(|t| mysql_interval_unit(t).is_some())?;
24190        Some((alloc::format!("{sign}{n}"), offset + 1))
24191    }
24192
24193    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24194    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24195    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24196    ///
24197    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24198    /// this by parsing the group and then restoring `self.pos` — which could
24199    /// never have worked, because `advance()` DESTROYS the token it returns
24200    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24201    /// inert only because both branches errored back then.
24202    fn interval_paren_is_quantity(&self) -> bool {
24203        let mut depth = 0usize;
24204        let mut saw_top_level_comma = false;
24205        let mut i = self.pos;
24206        while let Some(tok) = self.tokens.get(i) {
24207            match tok {
24208                Token::LParen => depth += 1,
24209                Token::RParen => {
24210                    depth = depth.saturating_sub(1);
24211                    if depth == 0 {
24212                        return !saw_top_level_comma
24213                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24214                                .is_some();
24215                    }
24216                }
24217                // A comma directly inside the outermost parens means the
24218                // argument list of the INTERVAL() function.
24219                Token::Comma if depth == 1 => saw_top_level_comma = true,
24220                Token::Eof => return false,
24221                _ => {}
24222            }
24223            i += 1;
24224        }
24225        false
24226    }
24227
24228    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24229        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24230        // (the index of the last Ni ≤ N), distinct from the interval literal.
24231        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24232        // is decided by a non-destructive lookahead (round 422) before either
24233        // branch consumes anything. MySQL only.
24234        if self.mysql_dialect
24235            && matches!(self.peek(), Token::LParen)
24236            && !self.interval_paren_is_quantity()
24237        {
24238            self.advance(); // (
24239            let mut args = Vec::new();
24240            if !matches!(self.peek(), Token::RParen) {
24241                loop {
24242                    args.push(self.parse_expr(0)?);
24243                    if matches!(self.peek(), Token::Comma) {
24244                        self.advance();
24245                        continue;
24246                    }
24247                    break;
24248                }
24249            }
24250            if !matches!(self.peek(), Token::RParen) {
24251                return Err(self.err(alloc::format!(
24252                    "expected ')' after INTERVAL() arguments, got {:?}",
24253                    self.peek()
24254                )));
24255            }
24256            self.advance(); // )
24257            return Ok(Expr::FunctionCall {
24258                name: alloc::string::String::from("interval"),
24259                args,
24260            });
24261        }
24262        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24263        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24264        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24265        // writes every date arithmetic there is, and it did not parse at
24266        // all. PG rejects the unquoted form outright (`syntax error at or
24267        // near "1"`, measured), so it is taken only in the MySQL dialect —
24268        // PG's own `INTERVAL '1' DAY` is untouched below.
24269        if self.mysql_dialect
24270            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24271        {
24272            for _ in 0..consume {
24273                self.advance(); // the optional `-` and the number
24274            }
24275            let Some(unit) = mysql_interval_unit(self.peek()) else {
24276                return Err(self.err(alloc::format!(
24277                    "expected an interval unit after INTERVAL {text}, got {:?}",
24278                    self.peek()
24279                )));
24280            };
24281            self.advance(); // the unit
24282            let (months, days, micros) = scale_mysql_interval(&text, unit)
24283                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24284            return Ok(Expr::Literal(Literal::Interval {
24285                months,
24286                days,
24287                micros,
24288                // The canonical rendering, so Display round-trips into a
24289                // form both dialects read back.
24290                text: alloc::format!("{text} {unit}"),
24291            }));
24292        }
24293        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24294        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24295        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24296        // Those cannot fold into a compile-time `Literal::Interval`, so they
24297        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24298        // builtin, which builds the value at run time (and yields NULL for a
24299        // NULL quantity, as MariaDB does). The literal path above still folds
24300        // the constant case — it is cheaper and round-trips through Display.
24301        //
24302        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24303        // MySQL's quoted spelling) keep the qualifier path below.
24304        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24305            let qty = self.parse_expr(0)?;
24306            let Some(unit) = mysql_interval_unit(self.peek()) else {
24307                return Err(self.err(alloc::format!(
24308                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24309                    self.peek()
24310                )));
24311            };
24312            self.advance(); // the unit
24313            return Ok(make_interval_call(qty, unit));
24314        }
24315        let tok = self.advance();
24316        let Token::String(text) = tok else {
24317            return Err(self.err(format!(
24318                "expected string literal after INTERVAL, got {tok:?}"
24319            )));
24320        };
24321        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24322        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24323        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24324        // bare number means and the leading/trailing precision.
24325        let field1 = interval_field_of(self.peek());
24326        let qualifier = if let Some(f1) = field1 {
24327            self.advance();
24328            let f2 = if matches!(self.peek(), Token::To) {
24329                self.advance();
24330                let Some(f) = interval_field_of(self.peek()) else {
24331                    return Err(self.err(format!(
24332                        "expected an interval field after TO, got {:?}",
24333                        self.peek()
24334                    )));
24335                };
24336                self.advance();
24337                Some(f)
24338            } else {
24339                None
24340            };
24341            Some((f1, f2))
24342        } else {
24343            None
24344        };
24345        let (months, days, micros) = match qualifier {
24346            Some(q) => interpret_qualified_interval(&text, q),
24347            None => parse_interval_text(&text),
24348        }
24349        .ok_or_else(|| ParseError {
24350            message: format!(
24351                "cannot parse INTERVAL {text:?}; \
24352                     expected `<n> <unit> [<n> <unit> ...]` with units \
24353                     microsecond[s], millisecond[s], second[s], minute[s], \
24354                     hour[s], day[s], week[s], month[s], year[s]"
24355            ),
24356            token_pos: self.consumed_pos(),
24357        })?;
24358        Ok(Expr::Literal(Literal::Interval {
24359            months,
24360            days,
24361            micros,
24362            text,
24363        }))
24364    }
24365
24366    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24367    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24368    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24369    /// than a pgvector literal.
24370    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24371        self.advance(); // consume `[`
24372        let mut items: Vec<Expr> = Vec::new();
24373        if !matches!(self.peek(), Token::RBracket) {
24374            loop {
24375                if matches!(self.peek(), Token::LBracket) {
24376                    items.push(self.parse_array_bracket_body()?);
24377                } else {
24378                    items.push(self.parse_expr(0)?);
24379                }
24380                match self.peek() {
24381                    Token::Comma => {
24382                        self.advance();
24383                    }
24384                    Token::RBracket => break,
24385                    other => {
24386                        return Err(self.err(alloc::format!(
24387                            "expected ',' or ']' in array literal, got {other:?}"
24388                        )));
24389                    }
24390                }
24391            }
24392        }
24393        self.advance(); // consume `]`
24394        Ok(Expr::Array(items))
24395    }
24396
24397    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24398        let mut elems = Vec::new();
24399        if matches!(self.peek(), Token::RBracket) {
24400            self.advance();
24401            return Ok(Expr::Literal(Literal::Vector(elems)));
24402        }
24403        loop {
24404            let e = self.parse_expr(0)?;
24405            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24406                message: format!("vector element must be a numeric literal, got {e:?}"),
24407                token_pos: self.pos,
24408            })?;
24409            elems.push(x);
24410            match self.peek() {
24411                Token::Comma => {
24412                    self.advance();
24413                }
24414                Token::RBracket => {
24415                    self.advance();
24416                    break;
24417                }
24418                other => {
24419                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24420                }
24421            }
24422        }
24423        Ok(Expr::Literal(Literal::Vector(elems)))
24424    }
24425
24426    /// Atom that started with an identifier: could be `t.col`, `col`, or
24427    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24428    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24429    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24430    /// is optional; an empty `()` is also legal (PG semantics).
24431    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24432    /// modifier between `name(args)` and `OVER (...)`. Default is
24433    /// `Respect`. Unrecognised idents leave the stream unchanged.
24434    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24435        let Token::Ident(s) = self.peek().clone() else {
24436            return NullTreatment::Respect;
24437        };
24438        let is_ignore = s.eq_ignore_ascii_case("ignore");
24439        let is_respect = s.eq_ignore_ascii_case("respect");
24440        if !is_ignore && !is_respect {
24441            return NullTreatment::Respect;
24442        }
24443        // Lookahead for NULLS — only consume both tokens together.
24444        // pos+1 must hold a "nulls" ident.
24445        if self.pos + 1 < self.tokens.len()
24446            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24447            && s2.eq_ignore_ascii_case("nulls")
24448        {
24449            self.advance();
24450            self.advance();
24451            return if is_ignore {
24452                NullTreatment::Ignore
24453            } else {
24454                NullTreatment::Respect
24455            };
24456        }
24457        NullTreatment::Respect
24458    }
24459
24460    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24461    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24462    /// (same shape as the `OVER` tail). Consumes the whole clause and
24463    /// returns the predicate; returns `None` when no `FILTER` follows.
24464    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24465        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24466            return Ok(None);
24467        };
24468        if !s.eq_ignore_ascii_case("filter") {
24469            return Ok(None);
24470        }
24471        self.advance(); // FILTER
24472        if !matches!(self.peek(), Token::LParen) {
24473            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24474        }
24475        self.advance(); // (
24476        if !matches!(self.peek(), Token::Where) {
24477            return Err(self.err(format!(
24478                "expected WHERE inside FILTER (...), got {:?}",
24479                self.peek()
24480            )));
24481        }
24482        self.advance(); // WHERE
24483        let cond = self.parse_expr(0)?;
24484        if !matches!(self.peek(), Token::RParen) {
24485            return Err(self.err(format!(
24486                "expected ')' to close FILTER (WHERE ...), got {:?}",
24487                self.peek()
24488            )));
24489        }
24490        self.advance(); // )
24491        Ok(Some(Box::new(cond)))
24492    }
24493
24494    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24495    /// the separator as the aggregate's second argument, which is the
24496    /// shape `string_agg` already takes. Returns whether one was there.
24497    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24498        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24499            return Ok(false);
24500        }
24501        self.advance();
24502        let Token::String(sep) = self.peek().clone() else {
24503            return Err(self.err(alloc::format!(
24504                "expected a string literal after SEPARATOR, got {:?}",
24505                self.peek()
24506            )));
24507        };
24508        self.advance();
24509        args.push(Expr::Literal(Literal::String(sep)));
24510        Ok(true)
24511    }
24512
24513    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24514    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24515    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24516    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24517    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24518        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24519            return Ok(Vec::new());
24520        };
24521        if !s.eq_ignore_ascii_case("within") {
24522            return Ok(Vec::new());
24523        }
24524        self.advance(); // WITHIN
24525        if !matches!(self.peek(), Token::Group) {
24526            return Err(self.err(format!(
24527                "expected GROUP after WITHIN, got {:?}",
24528                self.peek()
24529            )));
24530        }
24531        self.advance(); // GROUP
24532        if !matches!(self.peek(), Token::LParen) {
24533            return Err(self.err(format!(
24534                "expected '(' after WITHIN GROUP, got {:?}",
24535                self.peek()
24536            )));
24537        }
24538        self.advance(); // (
24539        if !matches!(self.peek(), Token::Order) {
24540            return Err(self.err(format!(
24541                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24542                self.peek()
24543            )));
24544        }
24545        self.advance(); // ORDER
24546        if !self.peek_is_by() {
24547            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24548        }
24549        self.advance(); // BY
24550        let mut keys: Vec<OrderBy> = Vec::new();
24551        loop {
24552            // v7.39 (round 691) — save/restore, the discipline this parser
24553            // already uses around `pending_sample_preds`, so a subquery inside
24554            // a key neither inherits nor leaks the channel.
24555            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24556            let saved_coll = self.order_key_collation.take();
24557            let parsed = self.parse_expr(0);
24558            self.in_order_by_key = saved_flag;
24559            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24560            let expr = parsed?;
24561            let desc = if matches!(self.peek(), Token::Desc) {
24562                self.advance();
24563                true
24564            } else if matches!(self.peek(), Token::Asc) {
24565                self.advance();
24566                false
24567            } else {
24568                false
24569            };
24570            let nulls_first = self.parse_optional_nulls_placement()?;
24571            keys.push(OrderBy {
24572                expr,
24573                desc,
24574                nulls_first,
24575                collation,
24576            });
24577            if matches!(self.peek(), Token::Comma) {
24578                self.advance();
24579            } else {
24580                break;
24581            }
24582        }
24583        if !matches!(self.peek(), Token::RParen) {
24584            return Err(self.err(format!(
24585                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24586                self.peek()
24587            )));
24588        }
24589        self.advance(); // )
24590        Ok(keys)
24591    }
24592
24593    /// No frame clause is supported.
24594    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24595    fn parse_over_clause(
24596        &mut self,
24597    ) -> Result<
24598        (
24599            Vec<Expr>,
24600            Vec<(Expr, bool, Option<bool>)>,
24601            Option<WindowFrame>,
24602        ),
24603        ParseError,
24604    > {
24605        // `OVER w` — a named-window reference. The WINDOW clause
24606        // parses after the select list, so the name rides out as a
24607        // marker in partition_by; parse_bare_select substitutes the
24608        // definition once the clause is known.
24609        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24610            let name = w.clone();
24611            self.advance();
24612            return Ok((
24613                alloc::vec![Expr::Column(crate::ast::ColumnName {
24614                    qualifier: Some("__named_window__".to_string()),
24615                    name,
24616                })],
24617                Vec::new(),
24618                None,
24619            ));
24620        }
24621        if !matches!(self.peek(), Token::LParen) {
24622            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24623        }
24624        self.advance();
24625        let mut partition_by = Vec::new();
24626        let mut order_by = Vec::new();
24627        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24628        // window, refined in place. PG's rules (probed against 18.4) differ
24629        // from the bare `OVER w1` form, so the reference rides out under its
24630        // own marker and `substitute_named_windows` applies them. The base
24631        // name is any leading identifier that isn't a window-spec keyword.
24632        let base_window = match self.peek() {
24633            Token::Ident(s) | Token::QuotedIdent(s)
24634                if !s.eq_ignore_ascii_case("partition")
24635                    && !s.eq_ignore_ascii_case("rows")
24636                    && !s.eq_ignore_ascii_case("range")
24637                    && !s.eq_ignore_ascii_case("groups") =>
24638            {
24639                let n = s.clone();
24640                self.advance();
24641                Some(n)
24642            }
24643            _ => None,
24644        };
24645        // PARTITION BY ?
24646        // v7.37.6-B promoted PARTITION to a reserved keyword
24647        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24648        // `Token::Ident("partition")`. Accept both so older sources
24649        // and the new lexer surface land on the same path.
24650        let is_partition_kw = match self.peek() {
24651            Token::Partition => true,
24652            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24653            _ => false,
24654        };
24655        if is_partition_kw {
24656            self.advance();
24657            if !self.peek_is_by() {
24658                return Err(self.err(format!(
24659                    "expected BY after PARTITION, got {:?}",
24660                    self.peek()
24661                )));
24662            }
24663            self.advance();
24664            loop {
24665                partition_by.push(self.parse_expr(0)?);
24666                if matches!(self.peek(), Token::Comma) {
24667                    self.advance();
24668                    continue;
24669                }
24670                break;
24671            }
24672        }
24673        // ORDER BY ?
24674        if matches!(self.peek(), Token::Order) {
24675            self.advance();
24676            if !self.peek_is_by() {
24677                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24678            }
24679            self.advance();
24680            loop {
24681                let e = self.parse_expr(0)?;
24682                let desc = if matches!(self.peek(), Token::Desc) {
24683                    self.advance();
24684                    true
24685                } else if matches!(self.peek(), Token::Asc) {
24686                    self.advance();
24687                    false
24688                } else {
24689                    false
24690                };
24691                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24692                let nulls_first = self.parse_optional_nulls_placement()?;
24693                order_by.push((e, desc, nulls_first));
24694                if matches!(self.peek(), Token::Comma) {
24695                    self.advance();
24696                    continue;
24697                }
24698                break;
24699            }
24700        }
24701        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24702        // Both keywords come through the lexer as identifiers; match
24703        // case-insensitively.
24704        let mut frame: Option<WindowFrame> = None;
24705        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24706            let kind = if s.eq_ignore_ascii_case("rows") {
24707                Some(FrameKind::Rows)
24708            } else if s.eq_ignore_ascii_case("range") {
24709                Some(FrameKind::Range)
24710            } else if s.eq_ignore_ascii_case("groups") {
24711                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24712                Some(FrameKind::Groups)
24713            } else {
24714                None
24715            };
24716            if let Some(kind) = kind {
24717                self.advance();
24718                frame = Some(self.parse_frame_tail(kind)?);
24719            }
24720        }
24721        if !matches!(self.peek(), Token::RParen) {
24722            return Err(self.err(format!(
24723                "expected ')' to close OVER clause, got {:?}",
24724                self.peek()
24725            )));
24726        }
24727        self.advance();
24728        if let Some(base) = base_window {
24729            // A copy may refine but never override the base's partitioning
24730            // (PG rejects it outright, before looking the name up).
24731            if !partition_by.is_empty() {
24732                return Err(self.err(alloc::format!(
24733                    "cannot override PARTITION BY clause of window \"{base}\""
24734                )));
24735            }
24736            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24737                qualifier: Some("__named_window_ref__".to_string()),
24738                name: base,
24739            })];
24740        }
24741        Ok((partition_by, order_by, frame))
24742    }
24743
24744    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24745    /// or `RANGE` keyword was just consumed. Accepts both
24746    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24747    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24748    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24749    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24750        let (start, end) = if matches!(self.peek(), Token::Between) {
24751            self.advance();
24752            let start = self.parse_frame_bound()?;
24753            if !matches!(self.peek(), Token::And) {
24754                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24755            }
24756            self.advance();
24757            let end = self.parse_frame_bound()?;
24758            (start, Some(end))
24759        } else {
24760            (self.parse_frame_bound()?, None)
24761        };
24762        let exclude = self.parse_frame_exclusion()?;
24763        Ok(WindowFrame {
24764            kind,
24765            start,
24766            end,
24767            exclude,
24768        })
24769    }
24770
24771    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24772    /// after a frame spec. NO OTHERS is the default no-op.
24773    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24774        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24775            return Ok(FrameExclusion::NoOthers);
24776        }
24777        self.advance(); // EXCLUDE
24778        match self.peek() {
24779            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24780                self.advance();
24781                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24782                    return Err(self.err(format!(
24783                        "expected ROW after EXCLUDE CURRENT, got {:?}",
24784                        self.peek()
24785                    )));
24786                }
24787                self.advance();
24788                Ok(FrameExclusion::CurrentRow)
24789            }
24790            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24791            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24792            // Without this arm it fell to the catch-all, whose message
24793            // self-contradictingly listed GROUP as expected.
24794            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24795                self.advance();
24796                Ok(FrameExclusion::Group)
24797            }
24798            Token::Group => {
24799                self.advance();
24800                Ok(FrameExclusion::Group)
24801            }
24802            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24803                self.advance();
24804                Ok(FrameExclusion::Ties)
24805            }
24806            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24807                self.advance();
24808                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24809                    return Err(self.err(format!(
24810                        "expected OTHERS after EXCLUDE NO, got {:?}",
24811                        self.peek()
24812                    )));
24813                }
24814                self.advance();
24815                Ok(FrameExclusion::NoOthers)
24816            }
24817            other => Err(self.err(format!(
24818                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24819            ))),
24820        }
24821    }
24822
24823    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24824    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24825    /// `UNBOUNDED FOLLOWING`.
24826    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24827        // Interval-typed offset for a value-based RANGE frame over a
24828        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24829        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24830        // PRECEDING`.
24831        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24832            let dir = self.expect_ident_like()?;
24833            return if dir.eq_ignore_ascii_case("preceding") {
24834                Ok(FrameBound::IntervalPreceding {
24835                    months,
24836                    days,
24837                    micros,
24838                })
24839            } else if dir.eq_ignore_ascii_case("following") {
24840                Ok(FrameBound::IntervalFollowing {
24841                    months,
24842                    days,
24843                    micros,
24844                })
24845            } else {
24846                Err(self.err(format!(
24847                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24848                )))
24849            };
24850        }
24851        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24852        if let Token::Integer(n) = *self.peek() {
24853            self.advance();
24854            let n: u64 = u64::try_from(n).map_err(|_| {
24855                self.err(format!(
24856                    "invalid frame offset {n} — expected non-negative integer"
24857                ))
24858            })?;
24859            let dir = self.expect_ident_like()?;
24860            return if dir.eq_ignore_ascii_case("preceding") {
24861                Ok(FrameBound::OffsetPreceding(n))
24862            } else if dir.eq_ignore_ascii_case("following") {
24863                Ok(FrameBound::OffsetFollowing(n))
24864            } else {
24865                Err(self.err(format!(
24866                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24867                )))
24868            };
24869        }
24870        let first = self.expect_ident_like()?;
24871        if first.eq_ignore_ascii_case("unbounded") {
24872            let dir = self.expect_ident_like()?;
24873            return if dir.eq_ignore_ascii_case("preceding") {
24874                Ok(FrameBound::UnboundedPreceding)
24875            } else if dir.eq_ignore_ascii_case("following") {
24876                Ok(FrameBound::UnboundedFollowing)
24877            } else {
24878                Err(self.err(format!(
24879                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24880                )))
24881            };
24882        }
24883        if first.eq_ignore_ascii_case("current") {
24884            let row = self.expect_ident_like()?;
24885            if !row.eq_ignore_ascii_case("row") {
24886                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24887            }
24888            return Ok(FrameBound::CurrentRow);
24889        }
24890        Err(self.err(format!(
24891            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24892        )))
24893    }
24894
24895    /// Detect and consume a leading interval offset in a frame bound —
24896    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24897    /// `(months, days, micros)`. Leaves the cursor on the trailing
24898    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24899    /// when the next tokens are not an interval offset.
24900    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24901        // Shape A — `INTERVAL '1 day'`.
24902        if matches!(self.peek(), Token::Interval) {
24903            self.advance(); // INTERVAL
24904            let atom = self.parse_interval_atom()?;
24905            if let Expr::Literal(Literal::Interval {
24906                months,
24907                days,
24908                micros,
24909                ..
24910            }) = atom
24911            {
24912                return Ok(Some((months, days, micros)));
24913            }
24914            return Err(self.err("expected an interval literal in frame offset".to_string()));
24915        }
24916        // Shape B — `'1 day'::interval`. Look ahead for the exact
24917        // string / `::` / interval-target triple before committing.
24918        if let Token::String(text) = self.peek() {
24919            let target_is_interval = match self.tokens.get(self.pos + 2) {
24920                Some(Token::Interval) => true,
24921                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24922                _ => false,
24923            };
24924            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24925                && target_is_interval;
24926            if is_cast {
24927                let text = text.clone();
24928                self.advance(); // string
24929                self.advance(); // ::
24930                self.advance(); // interval
24931                let parts = parse_interval_text(&text).ok_or_else(|| {
24932                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24933                })?;
24934                return Ok(Some(parts));
24935            }
24936        }
24937        Ok(None)
24938    }
24939
24940    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24941        if matches!(self.peek(), Token::Dot) {
24942            self.advance();
24943            let name = self.expect_ident_like()?;
24944            // v7.14.0 — schema-qualified function call
24945            // `<schema>.<fn>(args)`. PG dumps emit
24946            // `pg_catalog.set_config(...)` in the preamble. SPG
24947            // is single-namespace: drop the schema prefix and
24948            // route the dispatch on the bare function name.
24949            if matches!(self.peek(), Token::LParen) {
24950                return self.finish_ident_atom(name);
24951            }
24952            return Ok(Expr::Column(ColumnName {
24953                qualifier: Some(first),
24954                name,
24955            }));
24956        }
24957        if matches!(self.peek(), Token::LParen) {
24958            self.advance();
24959            // `COUNT(*)` — special-cased here because `*` isn't a normal
24960            // expression token. Lower-case match on `first` since the lexer
24961            // folds identifiers.
24962            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24963                self.advance();
24964                if !matches!(self.peek(), Token::RParen) {
24965                    return Err(self.err(format!(
24966                        "expected ')' after COUNT(*), got {:?}",
24967                        self.peek()
24968                    )));
24969                }
24970                self.advance();
24971                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24972                let filter = self.parse_filter_clause()?;
24973                // v4.12: COUNT(*) OVER (...) — same window tail.
24974                let null_treatment = self.parse_null_treatment_modifier();
24975                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24976                    && s.eq_ignore_ascii_case("over")
24977                {
24978                    self.advance();
24979                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
24980                    return Ok(Expr::WindowFunction {
24981                        name: "count_star".into(),
24982                        args: Vec::new(),
24983                        partition_by,
24984                        order_by,
24985                        frame,
24986                        null_treatment,
24987                        filter,
24988                    });
24989                }
24990                if let Some(filter) = filter {
24991                    return Ok(Expr::AggregateOrdered {
24992                        call: Box::new(Expr::FunctionCall {
24993                            name: "count_star".into(),
24994                            args: Vec::new(),
24995                        }),
24996                        order_by: Vec::new(),
24997                        distinct: false,
24998                        filter: Some(filter),
24999                    });
25000                }
25001                return Ok(Expr::FunctionCall {
25002                    name: "count_star".into(),
25003                    args: Vec::new(),
25004                });
25005            }
25006            // Function call. PG-style: zero-or-more comma-separated args.
25007            let mut args = Vec::new();
25008            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25009            // Names are collected in lock-step with `args` and resolved to
25010            // positional order after the loop (the AST stays positional).
25011            let mut arg_names: Vec<Option<String>> = Vec::new();
25012            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25013            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25014            // seen, so the value arguments before it can be folded.
25015            let mut saw_separator = false;
25016            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25017            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25018            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25019            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25020                self.advance();
25021                true
25022            } else if matches!(self.peek(), Token::All) {
25023                self.advance();
25024                false
25025            } else {
25026                false
25027            };
25028            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25029            // TIMESTAMPDIFF take a bare unit keyword as the first
25030            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25031            // bare type keyword (DATE / TIME / DATETIME); lower them
25032            // onto string literals so the evaluator sees plain text.
25033            if ((first.eq_ignore_ascii_case("timestampadd")
25034                || first.eq_ignore_ascii_case("timestampdiff"))
25035                && matches!(self.peek(), Token::Ident(u) if matches!(
25036                    u.to_ascii_lowercase().as_str(),
25037                    "microsecond" | "second" | "minute" | "hour" | "day"
25038                        | "week" | "month" | "quarter" | "year"
25039                )))
25040                || (first.eq_ignore_ascii_case("get_format")
25041                    && matches!(self.peek(), Token::Ident(u) if matches!(
25042                        u.to_ascii_lowercase().as_str(),
25043                        "date" | "time" | "datetime" | "timestamp"
25044                    )))
25045            {
25046                if let Token::Ident(u) = self.peek() {
25047                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25048                }
25049                self.advance();
25050                if matches!(self.peek(), Token::Comma) {
25051                    self.advance();
25052                }
25053            }
25054            // `ROW(a, b, …)` keyword constructor. Followed by a
25055            // comparison operator or [NOT] IN it joins the paren
25056            // row-constructor machinery (fieldwise parse-time
25057            // expansion); bare, it stays a `row` call the evaluator
25058            // renders as PG record text.
25059            if first.eq_ignore_ascii_case("row") {
25060                let mut row_items = Vec::new();
25061                if !matches!(self.peek(), Token::RParen) {
25062                    loop {
25063                        row_items.push(self.parse_expr(0)?);
25064                        match self.peek() {
25065                            Token::Comma => {
25066                                self.advance();
25067                            }
25068                            Token::RParen => break,
25069                            other => {
25070                                return Err(self.err(format!(
25071                                    "expected ',' or ')' in ROW(...), got {other:?}"
25072                                )));
25073                            }
25074                        }
25075                    }
25076                }
25077                self.advance(); // ')'
25078                let comparison_follows = matches!(
25079                    self.peek(),
25080                    Token::Eq
25081                        | Token::NotEq
25082                        | Token::Lt
25083                        | Token::LtEq
25084                        | Token::Gt
25085                        | Token::GtEq
25086                        | Token::In
25087                ) || (matches!(self.peek(), Token::Not)
25088                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25089                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25090                if comparison_follows && !row_items.is_empty() {
25091                    return self.parse_row_comparison_tail(row_items);
25092                }
25093                return Ok(Expr::FunctionCall {
25094                    name: String::from("row"),
25095                    args: row_items,
25096                });
25097            }
25098            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25099            // the parse-mode keyword introduces the source text. SPG
25100            // carries XML as text, so both modes lower to __xmlparse(expr)
25101            // which validates well-formedness and returns Value::Xml.
25102            if first.eq_ignore_ascii_case("xmlparse")
25103                && matches!(self.peek(), Token::Ident(kw)
25104                    if kw.eq_ignore_ascii_case("document")
25105                        || kw.eq_ignore_ascii_case("content"))
25106            {
25107                let mode = match self.advance() {
25108                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25109                    _ => unreachable!("peeked an ident"),
25110                };
25111                let src = self.parse_expr(0)?;
25112                if !matches!(self.peek(), Token::RParen) {
25113                    return Err(self.err(format!(
25114                        "expected ')' to close XMLPARSE, got {:?}",
25115                        self.peek()
25116                    )));
25117                }
25118                self.advance();
25119                return Ok(Expr::FunctionCall {
25120                    name: String::from("__xmlparse"),
25121                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25122                });
25123            }
25124            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25125            // keyword introduces the element name (a bare or quoted
25126            // identifier), then optional content expressions. Lower to a
25127            // plain `xmlelement(name_text, content …)` call.
25128            if first.eq_ignore_ascii_case("xmlelement")
25129                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25130            {
25131                self.advance(); // consume NAME
25132                let elem_name = match self.peek().clone() {
25133                    Token::Ident(n) | Token::QuotedIdent(n) => {
25134                        self.advance();
25135                        n
25136                    }
25137                    other => {
25138                        return Err(self.err(format!(
25139                            "expected element name after XMLELEMENT NAME, got {other:?}"
25140                        )));
25141                    }
25142                };
25143                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25144                while matches!(self.peek(), Token::Comma) {
25145                    self.advance();
25146                    args.push(self.parse_expr(0)?);
25147                }
25148                if !matches!(self.peek(), Token::RParen) {
25149                    return Err(self.err(format!(
25150                        "expected ')' to close XMLELEMENT, got {:?}",
25151                        self.peek()
25152                    )));
25153                }
25154                self.advance();
25155                return Ok(Expr::FunctionCall {
25156                    name: String::from("xmlelement"),
25157                    args,
25158                });
25159            }
25160            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25161            // becomes a `<name>value</name>` element; a bare column infers its
25162            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25163            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25164                let mut args: Vec<Expr> = Vec::new();
25165                loop {
25166                    let val = self.parse_expr(0)?;
25167                    let name = if matches!(self.peek(), Token::As) {
25168                        self.advance();
25169                        match self.peek().clone() {
25170                            Token::Ident(n) | Token::QuotedIdent(n) => {
25171                                self.advance();
25172                                n
25173                            }
25174                            other => {
25175                                return Err(self.err(format!(
25176                                    "expected name after AS in XMLFOREST, got {other:?}"
25177                                )));
25178                            }
25179                        }
25180                    } else if let Expr::Column(c) = &val {
25181                        c.name.clone()
25182                    } else {
25183                        return Err(
25184                            self.err("XMLFOREST element without a column name needs AS".into())
25185                        );
25186                    };
25187                    args.push(Expr::Literal(Literal::String(name)));
25188                    args.push(val);
25189                    if matches!(self.peek(), Token::Comma) {
25190                        self.advance();
25191                    } else {
25192                        break;
25193                    }
25194                }
25195                if !matches!(self.peek(), Token::RParen) {
25196                    return Err(self.err(format!(
25197                        "expected ')' to close XMLFOREST, got {:?}",
25198                        self.peek()
25199                    )));
25200                }
25201                self.advance();
25202                return Ok(Expr::FunctionCall {
25203                    name: String::from("xmlforest"),
25204                    args,
25205                });
25206            }
25207            // SQL-standard `POSITION(sub IN str)` — lowers onto
25208            // strpos(str, sub). IN is the argument separator here,
25209            // so the needle parses with the IN-tail suppressed.
25210            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25211                let saved = self.suppress_in_tail;
25212                self.suppress_in_tail = true;
25213                let needle = self.parse_expr(0);
25214                self.suppress_in_tail = saved;
25215                let needle = needle?;
25216                if matches!(self.peek(), Token::In) {
25217                    self.advance();
25218                    let haystack = self.parse_expr(0)?;
25219                    if !matches!(self.peek(), Token::RParen) {
25220                        return Err(self.err(format!(
25221                            "expected ')' to close POSITION, got {:?}",
25222                            self.peek()
25223                        )));
25224                    }
25225                    self.advance();
25226                    return Ok(Expr::FunctionCall {
25227                        name: String::from("strpos"),
25228                        args: alloc::vec![haystack, needle],
25229                    });
25230                }
25231                // position(sub, str) comma form (incl. bytea) —
25232                // hand the parsed first arg to the generic list.
25233                args.push(needle);
25234                if matches!(self.peek(), Token::Comma) {
25235                    self.advance();
25236                }
25237            }
25238            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25239            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25240            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25241            // riding the generic argument list below.
25242            if first.eq_ignore_ascii_case("trim") {
25243                let mode = match self.peek() {
25244                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25245                        self.advance();
25246                        Some("btrim")
25247                    }
25248                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25249                        self.advance();
25250                        Some("ltrim")
25251                    }
25252                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25253                        self.advance();
25254                        Some("rtrim")
25255                    }
25256                    _ => None,
25257                };
25258                if mode.is_some() || matches!(self.peek(), Token::From) {
25259                    // TRIM([mode] FROM str) — no strip-chars.
25260                    let chars = if matches!(self.peek(), Token::From) {
25261                        None
25262                    } else {
25263                        Some(self.parse_expr(0)?)
25264                    };
25265                    if !matches!(self.peek(), Token::From) {
25266                        return Err(self.err(format!(
25267                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25268                            self.peek()
25269                        )));
25270                    }
25271                    self.advance();
25272                    let target = self.parse_expr(0)?;
25273                    if !matches!(self.peek(), Token::RParen) {
25274                        return Err(
25275                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25276                        );
25277                    }
25278                    self.advance();
25279                    let mut trim_args = alloc::vec![target];
25280                    if let Some(c) = chars {
25281                        trim_args.push(c);
25282                    }
25283                    return Ok(Expr::FunctionCall {
25284                        name: String::from(mode.unwrap_or("btrim")),
25285                        args: trim_args,
25286                    });
25287                }
25288            }
25289            if !matches!(self.peek(), Token::RParen) {
25290                loop {
25291                    // v7.38 (read01, T14) — `argname => value` names this arg.
25292                    // v7.39 (read01 round 77) — `argname := value` is the same
25293                    // thing, and it is the spelling PG's own docs lead with. It
25294                    // was simply never lexed here, so every `f(x := 1)` died in
25295                    // the parser regardless of what `f` was.
25296                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25297                        (
25298                            Token::Ident(n) | Token::QuotedIdent(n),
25299                            Some(Token::FatArrow | Token::ColonEq),
25300                        ) => {
25301                            let name = n.clone();
25302                            self.advance(); // name
25303                            self.advance(); // => / :=
25304                            Some(name)
25305                        }
25306                        _ => None,
25307                    };
25308                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25309                    // array's elements into a variadic call's trailing args
25310                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25311                    // reserved, so it arrives as a bare ident before the arg.
25312                    let is_variadic = this_name.is_none()
25313                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25314                    if is_variadic {
25315                        self.advance();
25316                    }
25317                    let arg = self.parse_expr(0)?;
25318                    args.push(match &this_name {
25319                        // The callee's parameter names decide the slot, and a
25320                        // user function's live in the catalog. Carry the name
25321                        // to eval rather than guessing here.
25322                        Some(n) => Expr::NamedArg {
25323                            name: n.clone(),
25324                            expr: Box::new(arg),
25325                        },
25326                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25327                        None => arg,
25328                    });
25329                    arg_names.push(this_name);
25330                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25331                    // The `::` cast already worked; this lowers the
25332                    // function form onto the same Expr::Cast node.
25333                    if first.eq_ignore_ascii_case("cast")
25334                        && args.len() == 1
25335                        && matches!(self.peek(), Token::As)
25336                    {
25337                        self.advance();
25338                        let target = self.parse_cast_target()?;
25339                        if !matches!(self.peek(), Token::RParen) {
25340                            return Err(self.err(format!(
25341                                "expected ')' to close CAST, got {:?}",
25342                                self.peek()
25343                            )));
25344                        }
25345                        self.advance();
25346                        return Ok(Expr::Cast {
25347                            expr: Box::new(args.pop().expect("one arg")),
25348                            target,
25349                        });
25350                    }
25351                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25352                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25353                    // keywords; SPG's lexer makes them plain idents (so they'd be
25354                    // read as column refs). Lower the keyword to the string form
25355                    // the evaluator already accepts.
25356                    if first.eq_ignore_ascii_case("normalize")
25357                        && args.len() == 1
25358                        && matches!(self.peek(), Token::Comma)
25359                    {
25360                        let form = match self.tokens.get(self.pos + 1) {
25361                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25362                                let up = f.to_ascii_uppercase();
25363                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25364                            }
25365                            _ => None,
25366                        };
25367                        if let Some(up) = form {
25368                            self.advance(); // comma
25369                            self.advance(); // form keyword
25370                            args.push(Expr::Literal(Literal::String(up)));
25371                        }
25372                    }
25373                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25374                    // form. Desugars to the comma-list shape evaluator already
25375                    // handles. Triggered after the first arg when the function
25376                    // name is substring / substr and the next token is FROM
25377                    // (a reserved keyword in PG; SPG also reserves it).
25378                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25379                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25380                    // internal __substring_similar(str, pat, esc) call.
25381                    if (first.eq_ignore_ascii_case("substring")
25382                        || first.eq_ignore_ascii_case("substr"))
25383                        && args.len() == 1
25384                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25385                    {
25386                        self.advance(); // SIMILAR
25387                        let pattern = self.parse_expr(0)?;
25388                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25389                        {
25390                            return Err(self.err(format!(
25391                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25392                                self.peek()
25393                            )));
25394                        }
25395                        self.advance(); // ESCAPE
25396                        let esc = self.parse_expr(0)?;
25397                        if !matches!(self.peek(), Token::RParen) {
25398                            return Err(self.err(format!(
25399                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25400                                self.peek()
25401                            )));
25402                        }
25403                        self.advance();
25404                        args.push(pattern);
25405                        args.push(esc);
25406                        return Ok(Expr::FunctionCall {
25407                            name: "__substring_similar".to_string(),
25408                            args,
25409                        });
25410                    }
25411                    if (first.eq_ignore_ascii_case("substring")
25412                        || first.eq_ignore_ascii_case("substr"))
25413                        && args.len() == 1
25414                        && matches!(self.peek(), Token::From | Token::For)
25415                    {
25416                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25417                        // `substring(str FOR len)` which PG treats as FROM 1.
25418                        if matches!(self.peek(), Token::From) {
25419                            self.advance();
25420                            let start = self.parse_expr(0)?;
25421                            args.push(start);
25422                        } else {
25423                            args.push(Expr::Literal(Literal::Integer(1)));
25424                        }
25425                        if matches!(self.peek(), Token::For) {
25426                            self.advance();
25427                            let length = self.parse_expr(0)?;
25428                            args.push(length);
25429                        }
25430                        if !matches!(self.peek(), Token::RParen) {
25431                            return Err(self.err(format!(
25432                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25433                                self.peek()
25434                            )));
25435                        }
25436                        self.advance();
25437                        return Ok(Expr::FunctionCall {
25438                            name: first.to_ascii_lowercase(),
25439                            args,
25440                        });
25441                    }
25442                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25443                    // syntactic form. Desugars to the `overlay(str,
25444                    // repl, n[, len])` comma-list shape the evaluator
25445                    // already implements. `PLACING` is not a reserved
25446                    // token in SPG, so it arrives as a bare Ident.
25447                    if first.eq_ignore_ascii_case("overlay")
25448                        && args.len() == 1
25449                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25450                    {
25451                        self.advance(); // consume PLACING
25452                        args.push(self.parse_expr(0)?); // replacement
25453                        if !matches!(self.peek(), Token::From) {
25454                            return Err(self.err(format!(
25455                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25456                                self.peek()
25457                            )));
25458                        }
25459                        self.advance();
25460                        args.push(self.parse_expr(0)?); // start position
25461                        if matches!(self.peek(), Token::For) {
25462                            self.advance();
25463                            args.push(self.parse_expr(0)?); // length
25464                        }
25465                        if !matches!(self.peek(), Token::RParen) {
25466                            return Err(self.err(format!(
25467                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25468                                self.peek()
25469                            )));
25470                        }
25471                        self.advance();
25472                        return Ok(Expr::FunctionCall {
25473                            name: String::from("overlay"),
25474                            args,
25475                        });
25476                    }
25477                    // `TRIM(chars FROM str)` — the keyword-less
25478                    // spelling lands here after the chars parse
25479                    // (the keyword forms return earlier).
25480                    if first.eq_ignore_ascii_case("trim")
25481                        && args.len() == 1
25482                        && matches!(self.peek(), Token::From)
25483                    {
25484                        self.advance();
25485                        let target = self.parse_expr(0)?;
25486                        if !matches!(self.peek(), Token::RParen) {
25487                            return Err(self.err(format!(
25488                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25489                                self.peek()
25490                            )));
25491                        }
25492                        self.advance();
25493                        let chars = args.pop().expect("one arg");
25494                        return Ok(Expr::FunctionCall {
25495                            name: String::from("btrim"),
25496                            args: alloc::vec![target, chars],
25497                        });
25498                    }
25499                    // v7.24 (round-16 A) — aggregate-internal
25500                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25501                    // LAST)`. Keys close the argument list.
25502                    if matches!(self.peek(), Token::Order) {
25503                        self.advance();
25504                        if !self.peek_is_by() {
25505                            return Err(self.err(format!(
25506                                "expected BY after ORDER in aggregate args, got {:?}",
25507                                self.peek()
25508                            )));
25509                        }
25510                        self.advance();
25511                        loop {
25512                            // v7.39 (round 691) — save/restore, the discipline this parser
25513                            // already uses around `pending_sample_preds`, so a subquery inside
25514                            // a key neither inherits nor leaks the channel.
25515                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25516                            let saved_coll = self.order_key_collation.take();
25517                            let parsed = self.parse_expr(0);
25518                            self.in_order_by_key = saved_flag;
25519                            let collation =
25520                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25521                            let expr = parsed?;
25522                            let desc = if matches!(self.peek(), Token::Desc) {
25523                                self.advance();
25524                                true
25525                            } else if matches!(self.peek(), Token::Asc) {
25526                                self.advance();
25527                                false
25528                            } else {
25529                                false
25530                            };
25531                            let nulls_first = self.parse_optional_nulls_placement()?;
25532                            agg_order_by.push(OrderBy {
25533                                expr,
25534                                desc,
25535                                nulls_first,
25536                                collation,
25537                            });
25538                            if matches!(self.peek(), Token::Comma) {
25539                                self.advance();
25540                            } else {
25541                                break;
25542                            }
25543                        }
25544                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25545                        // follow the ORDER BY inside GROUP_CONCAT.
25546                        if self.consume_group_concat_separator(&mut args)? {
25547                            saw_separator = true;
25548                        }
25549                        if !matches!(self.peek(), Token::RParen) {
25550                            return Err(self.err(format!(
25551                                "expected ')' after aggregate ORDER BY, got {:?}",
25552                                self.peek()
25553                            )));
25554                        }
25555                        break;
25556                    }
25557                    // v7.39 (round 354, M12) — …or directly after the
25558                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25559                    // own spelling of what PG passes as string_agg's second
25560                    // argument; it was a parse error, so every MySQL query
25561                    // that names its own separator failed outright.
25562                    if self.consume_group_concat_separator(&mut args)? {
25563                        saw_separator = true;
25564                        break;
25565                    }
25566                    match self.peek() {
25567                        Token::Comma => {
25568                            self.advance();
25569                        }
25570                        Token::RParen => break,
25571                        other => {
25572                            return Err(self.err(format!(
25573                                "expected ',' or ')' in function args, got {other:?}"
25574                            )));
25575                        }
25576                    }
25577                }
25578            }
25579            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25580            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25581            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25582            // meaning a separator — that is what the explicit SEPARATOR
25583            // tail is for. Fold them into one `concat(...)` so the
25584            // aggregate keeps its single value argument.
25585            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25586                let values = args.len() - usize::from(saw_separator);
25587                if values > 1 {
25588                    let sep_arg = if saw_separator { args.pop() } else { None };
25589                    let folded = Expr::FunctionCall {
25590                        name: "concat".to_string(),
25591                        args: core::mem::take(&mut args),
25592                    };
25593                    args.push(folded);
25594                    if let Some(sep) = sep_arg {
25595                        args.push(sep);
25596                    }
25597                }
25598            }
25599            self.advance(); // consume ')'
25600            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25601            // any more. The parser has no catalog, so it could only ever resolve
25602            // the handful of `make_*` builtins whose parameter names were baked
25603            // into a table right here — every user function got
25604            // "does not support named arguments", though the catalog has been
25605            // storing its parameter names all along. Reordering happens in eval,
25606            // in one place, for builtins and user functions alike.
25607            // v7.32 (round-29) — ordered-set aggregate tail
25608            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25609            // (percentile_cont / percentile_disc / mode). The sort spec
25610            // lands in the same `order_by` slot a decorated aggregate
25611            // uses; the executor dispatches on the function name. WITHIN
25612            // GROUP and an intra-argument ORDER BY are mutually
25613            // exclusive (PG rejects both).
25614            let within_group_order = self.parse_within_group_clause()?;
25615            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25616                return Err(self.err(
25617                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25618                        .into(),
25619                ));
25620            }
25621            let within_group_seen = !within_group_order.is_empty();
25622            let agg_order_by = if within_group_order.is_empty() {
25623                agg_order_by
25624            } else {
25625                within_group_order
25626            };
25627            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25628            let filter = self.parse_filter_clause()?;
25629            // v4.12: window-function tail — `name(args) OVER (...)`.
25630            // Promotes the just-parsed FunctionCall into a
25631            // WindowFunction node carrying partition + order.
25632            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25633            // / `RESPECT NULLS OVER (...)` between the closing paren
25634            // and `OVER`.
25635            let null_treatment = self.parse_null_treatment_modifier();
25636            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25637                && s.eq_ignore_ascii_case("over")
25638            {
25639                self.advance();
25640                // v7.39 (round 230) — PG implements neither modifier for a
25641                // windowed call and says so (0A000). Both used to be parsed
25642                // and then silently dropped here, so `count(DISTINCT v)
25643                // OVER (…)` quietly answered the non-distinct count.
25644                if agg_distinct {
25645                    return Err(
25646                        self.err("DISTINCT is not implemented for window functions".to_string())
25647                    );
25648                }
25649                if !agg_order_by.is_empty() {
25650                    // PG separates the two shapes that land here: a
25651                    // WITHIN GROUP call is an ordered-set aggregate and gets
25652                    // its own message naming the aggregate; a plain
25653                    // `agg(x ORDER BY y)` gets the generic one.
25654                    let msg = if within_group_seen {
25655                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25656                    } else {
25657                        "aggregate ORDER BY is not implemented for window functions".to_string()
25658                    };
25659                    return Err(self.err(msg));
25660                }
25661                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25662                return Ok(Expr::WindowFunction {
25663                    name: first,
25664                    args,
25665                    partition_by,
25666                    order_by,
25667                    frame,
25668                    null_treatment,
25669                    filter,
25670                });
25671            }
25672            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25673                return Ok(Expr::AggregateOrdered {
25674                    call: Box::new(Expr::FunctionCall { name: first, args }),
25675                    order_by: agg_order_by,
25676                    distinct: agg_distinct,
25677                    filter,
25678                });
25679            }
25680            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25681            // over TIMESTAMPTZ and has no timestamp overload, so a
25682            // timestamp argument is coerced on the way in and the answer
25683            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25684            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25685            // zone`. SPG answered `timestamp without time zone`, dropping
25686            // the offset from every rendering.
25687            //
25688            // Writing the coercion PG performs makes the existing
25689            // argument-driven typing (the one `date_trunc` uses) reach the
25690            // right answer, rather than teaching the type layer a second
25691            // rule. MySQL's DATE_ADD is a different function that returns
25692            // DATE or DATETIME, so this is PG-dialect only.
25693            //
25694            // Out-of-line because this sits on the RECURSIVE descent
25695            // frame: an inline block with locals here costs every nesting
25696            // level, and the suite's deep-nesting sentinel overflowed the
25697            // 512 KiB parser stack the moment one was added (round 430's
25698            // lesson, in the same shape).
25699            if !self.mysql_dialect {
25700                lift_date_add_arg_to_timestamptz(&first, &mut args);
25701            }
25702            return Ok(Expr::FunctionCall { name: first, args });
25703        }
25704        // v7.9.20 — SQL-standard parenless keyword expressions
25705        // (PG treats these as functions called without parens).
25706        // Resolve to a synthetic FunctionCall so the engine's
25707        // eval path reuses the existing function-call routing.
25708        // mailrs G3.
25709        let lc = first.to_ascii_lowercase();
25710        if matches!(
25711            lc.as_str(),
25712            "current_date"
25713                | "current_time"
25714                | "current_timestamp"
25715                | "localtimestamp"
25716                | "localtime"
25717                // v7.37.17 (17.6 siblings) — session-identity SQL-
25718                // standard parenless keywords. current_user /
25719                // session_user / user were already caught by the
25720                // pgwire canned-response shortcut but bare-select
25721                // in the embedded engine went through Expr::Column
25722                // and errored. Adding them here so the parser
25723                // resolves to a synthetic FunctionCall that reuses
25724                // the existing eval/functions.rs dispatch.
25725                | "current_user"
25726                | "session_user"
25727                | "current_role"
25728                | "current_catalog"
25729                | "current_schema"
25730                | "current_database"
25731                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25732                | "system_user"
25733        ) {
25734            return Ok(Expr::FunctionCall {
25735                name: lc,
25736                args: Vec::new(),
25737            });
25738        }
25739        Ok(Expr::Column(ColumnName {
25740            qualifier: None,
25741            name: first,
25742        }))
25743    }
25744}
25745
25746/// v7.39 (round 522) — write the coercion PG's `date_add` /
25747/// `date_subtract` signature performs.
25748///
25749/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25750/// timestamp argument is cast on the way in and the answer is
25751/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25752/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25753/// `timestamp without time zone`, dropping the offset from every
25754/// rendering of the result.
25755///
25756/// Writing the cast the signature implies lets the existing
25757/// argument-driven typing (the one `date_trunc` uses) reach the right
25758/// answer instead of teaching the type layer a second rule. MySQL's
25759/// DATE_ADD is a different function returning DATE or DATETIME, so the
25760/// caller applies this in PG dialect only.
25761///
25762/// A free function, and not a block at the call site, because the caller
25763/// is on the recursive-descent frame chain.
25764#[inline(never)]
25765fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25766    if args.len() != 2
25767        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25768    {
25769        return;
25770    }
25771    let base = args.remove(0);
25772    args.insert(
25773        0,
25774        Expr::Cast {
25775            expr: Box::new(base),
25776            target: CastTarget::Timestamptz,
25777        },
25778    );
25779}
25780
25781/// v6.8.2 — walk an expression tree and return the first column
25782/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25783/// to derive `CreateIndexStatement.column` from an expression
25784/// key (so downstream planner code resolving a primary column
25785/// position keeps working with expression indexes). Returns
25786/// `None` when the expression has no column ref at all — caller
25787/// surfaces that as a parse error.
25788fn extract_first_column(expr: &Expr) -> Option<String> {
25789    match expr {
25790        Expr::Column(cn) => Some(cn.name.clone()),
25791        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25792        Expr::Binary { lhs, rhs, .. } => {
25793            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25794        }
25795        Expr::Unary { expr: e, .. } => extract_first_column(e),
25796        // v7.39 (read01 round 93) — a cast wraps its operand: a common
25797        // expression-index key is `lower(col::text)`, where the column
25798        // sits under the `::text` cast inside the function arg. Without
25799        // descending here the key was rejected as "references no column".
25800        Expr::Cast { expr: e, .. } => extract_first_column(e),
25801        _ => None,
25802    }
25803}
25804
25805fn maybe_not(expr: Expr, negated: bool) -> Expr {
25806    if negated {
25807        Expr::Unary {
25808            op: UnOp::Not,
25809            expr: Box::new(expr),
25810        }
25811    } else {
25812        expr
25813    }
25814}
25815
25816/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25817/// things in the two dialects, and SPG read all three PG's way:
25818///
25819/// | token | PG (and SPG) | MySQL, measured |
25820/// |---|---|---|
25821/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25822/// | `&&` | inet / array overlap | **AND** |
25823/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25824///
25825/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25826/// answer with no error, which is why they are routed here rather than
25827/// left to the shared table.
25828impl Parser {
25829    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25830        if self.mysql_dialect {
25831            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25832            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25833            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25834            if let Token::Ident(w) = tok
25835                && w.eq_ignore_ascii_case("div")
25836            {
25837                return Some((BinOp::IntDiv, 8));
25838            }
25839            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25840            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25841            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25842            // there sits in operand position, not infix).
25843            if let Token::Ident(w) = tok
25844                && w.eq_ignore_ascii_case("mod")
25845            {
25846                return Some((BinOp::Mod, 8));
25847            }
25848            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25849            // plain ident to the lexer. Its precedence sits between OR (1)
25850            // and AND (3) — hence rung 2, the slot freed by moving AND up.
25851            if let Token::Ident(w) = tok
25852                && w.eq_ignore_ascii_case("xor")
25853            {
25854                return Some((BinOp::LogicalXor, 2));
25855            }
25856            match tok {
25857                Token::Concat => return Some((BinOp::Or, 1)),
25858                // MySQL's `&&` is logical AND, sharing AND's rung (3).
25859                Token::InetOverlap => return Some((BinOp::And, 3)),
25860                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25861                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25862                _ => {}
25863            }
25864        }
25865        binop_from(tok)
25866    }
25867}
25868
25869// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25870// (which sits strictly between OR and AND), every level from AND upward was
25871// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25872// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25873// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25874// the *relative* order of every PG operator is unchanged by the shift.
25875fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25876    let pair = match tok {
25877        Token::Or => (BinOp::Or, 1),
25878        Token::And => (BinOp::And, 3),
25879        Token::Eq => (BinOp::Eq, 5),
25880        Token::NotEq => (BinOp::NotEq, 5),
25881        Token::Lt => (BinOp::Lt, 5),
25882        Token::LtEq => (BinOp::LtEq, 5),
25883        Token::Gt => (BinOp::Gt, 5),
25884        Token::GtEq => (BinOp::GtEq, 5),
25885        // pgvector distance ops all sit on the same rung — tighter than
25886        // comparisons (5) so `col <-> v < threshold` parses correctly.
25887        Token::L2Distance => (BinOp::L2Distance, 6),
25888        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25889        // comparison rung.
25890        Token::GeomParallel => (BinOp::GeomParallel, 5),
25891        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25892        // comparison rung.
25893        Token::OverLeft => (BinOp::OverLeft, 5),
25894        Token::OverRight => (BinOp::OverRight, 5),
25895        Token::GeomPerp => (BinOp::GeomPerp, 5),
25896        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25897        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25898        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25899        Token::InnerProduct => (BinOp::InnerProduct, 6),
25900        Token::CosineDistance => (BinOp::CosineDistance, 6),
25901        Token::Plus => (BinOp::Add, 7),
25902        Token::Minus => (BinOp::Sub, 7),
25903        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25904        // binds every "other" operator (`||`, `|`, `&`, `#`, the
25905        // pgvector distances above) BETWEEN additive (7) and the
25906        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25907        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25908        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25909        // ("matches PG conceptually" — the round-753 audit measured it
25910        // false; the old rung errored on `'a' || 1 + 1` with
25911        // `text + integer`). Same-level chains left-fold, as PG does.
25912        Token::Concat => (BinOp::Concat, 6),
25913        Token::Pipe => (BinOp::BitOr, 6),
25914        Token::Amp => (BinOp::BitAnd, 6),
25915        Token::Star => (BinOp::Mul, 8),
25916        Token::Slash => (BinOp::Div, 8),
25917        Token::Percent => (BinOp::Mod, 8),
25918        // v4.14: JSON path ops bind tighter than comparisons (5)
25919        // and additive (7) so `doc->'k' = 'v'` parses correctly.
25920        // Same rung as the multiplicative ops.
25921        Token::JsonGet => (BinOp::JsonGet, 8),
25922        Token::JsonGetText => (BinOp::JsonGetText, 8),
25923        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25924        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25925        Token::JsonContains => (BinOp::JsonContains, 8),
25926        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25927        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25928        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25929        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25930        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25931        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25932        // v7.12.2 — `@@` binds at the comparison rung (looser than
25933        // arithmetic, tighter than AND / OR). PG places `@@` at
25934        // the same precedence as `=` / `<`, so we follow.
25935        Token::TsMatch => (BinOp::TsMatch, 5),
25936        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25937        // PG places these at the comparison rung (same level as `=`),
25938        // so we follow.
25939        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25940        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25941        Token::InetContains => (BinOp::InetContains, 5),
25942        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25943        Token::InetOverlap => (BinOp::InetOverlap, 5),
25944        // v7.39 (round 508) — the geometric and pattern-order predicates
25945        // ride the comparison rung, as every other predicate does.
25946        Token::Intersects => (BinOp::Intersects, 5),
25947        Token::IsBelow => (BinOp::IsBelow, 5),
25948        Token::IsAbove => (BinOp::IsAbove, 5),
25949        Token::PatternLt => (BinOp::PatternLt, 5),
25950        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25951        Token::PatternGt => (BinOp::PatternGt, 5),
25952        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25953        // `@@@` is the old spelling of `@@` and means exactly it.
25954        Token::TsMatchOld => (BinOp::TsMatch, 5),
25955        _ => return None,
25956    };
25957    Some(pair)
25958}
25959
25960#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25961// `as f32` here is intentional: vector elements widen / narrow into f32 on
25962// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25963// past ~15 decimal digits — both are acceptable for a fixed-precision
25964// pgvector column.
25965/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25966/// implicit table alias and break trailing clauses. WITH lands
25967/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25968/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25969/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25970/// / VALUES / FOR / LATERAL — all of which would otherwise be
25971/// silently swallowed by `parse_optional_alias`.
25972fn is_alias_stopword(s: &str) -> bool {
25973    matches!(
25974        s.to_ascii_lowercase().as_str(),
25975        "with"
25976            | "on"
25977            | "where"
25978            | "having"
25979            | "group"
25980            | "order"
25981            | "limit"
25982            | "offset"
25983            | "union"
25984            | "except"
25985            | "intersect"
25986            | "returning"
25987            | "set"
25988            | "values"
25989            | "for"
25990            | "window"
25991            | "tablesample"
25992            | "lateral"
25993            | "left"
25994            | "right"
25995            | "inner"
25996            | "outer"
25997            | "full"
25998            | "cross"
25999            | "join"
26000            | "natural"
26001            | "using"
26002            | "fetch"
26003    )
26004}
26005
26006fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26007    match e {
26008        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26009        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26010        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26011        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26012        // so scale the divisor by hand instead of `f32::powi`.)
26013        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26014            let mut div = 1.0f32;
26015            for _ in 0..*scale {
26016                div *= 10.0;
26017            }
26018            Some(*unscaled as f32 / div)
26019        }
26020        Expr::Unary {
26021            op: UnOp::Neg,
26022            expr,
26023        } => extract_numeric_literal(expr).map(|x| -x),
26024        _ => None,
26025    }
26026}
26027
26028/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26029/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26030/// negative. Returns `None` if any pair fails to parse or no pair is found.
26031///
26032/// Recognised units (case-insensitive, optional trailing `s`):
26033/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26034/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26035/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26036/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26037/// (PG-canonical: DST and month-boundary semantics depend on this).
26038/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26039/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26040/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26041#[allow(clippy::cast_possible_truncation)]
26042fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26043    let mut months: i64 = 0;
26044    let mut days: i64 = 0;
26045    let mut micros: i64 = 0;
26046    let mut in_time = false;
26047    let mut num = alloc::string::String::new();
26048    for ch in rest.chars() {
26049        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26050            num.push(ch);
26051            continue;
26052        }
26053        if ch == 'T' || ch == 't' {
26054            if !num.is_empty() {
26055                return None;
26056            }
26057            in_time = true;
26058            continue;
26059        }
26060        let n: f64 = num.parse().ok()?;
26061        num.clear();
26062        match (ch, in_time) {
26063            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26064            ('M', false) => months += n as i64,
26065            ('W' | 'w', false) => days += (n * 7.0) as i64,
26066            ('D' | 'd', false) => days += n as i64,
26067            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26068            ('M', true) => micros += (n * 60_000_000.0) as i64,
26069            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26070            _ => return None,
26071        }
26072    }
26073    if !num.is_empty() {
26074        return None;
26075    }
26076    Some((
26077        i32::try_from(months).ok()?,
26078        i32::try_from(days).ok()?,
26079        micros,
26080    ))
26081}
26082
26083/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26084/// leading `-` negates the whole value). Rejects date-like strings.
26085fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26086    let (neg, body) = match s.strip_prefix('-') {
26087        Some(b) => (true, b),
26088        None => (false, s),
26089    };
26090    let (y, m) = body.split_once('-')?;
26091    let years: i32 = y.parse().ok()?;
26092    let mons: i32 = m.parse().ok()?;
26093    if years < 0 || mons < 0 {
26094        return None;
26095    }
26096    let total = years.checked_mul(12)?.checked_add(mons)?;
26097    Some((if neg { -total } else { total }, 0, 0))
26098}
26099
26100/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26101/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26102fn parse_interval_clock(tok: &str) -> Option<i64> {
26103    let (neg, body) = match tok.strip_prefix('-') {
26104        Some(r) => (true, r),
26105        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26106    };
26107    let mut it = body.split(':');
26108    let h: i64 = it.next()?.parse().ok()?;
26109    let m: i64 = it.next()?.parse().ok()?;
26110    let s_tok = it.next().unwrap_or("0");
26111    if it.next().is_some() {
26112        return None;
26113    }
26114    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26115        let sec: i64 = sec.parse().ok()?;
26116        let mut f = alloc::string::String::from(frac);
26117        while f.len() < 6 {
26118            f.push('0');
26119        }
26120        f.truncate(6);
26121        let fus: i64 = f.parse().ok()?;
26122        sec.checked_mul(1_000_000)?.checked_add(fus)?
26123    } else {
26124        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26125    };
26126    let total = h
26127        .checked_mul(3_600_000_000)?
26128        .checked_add(m.checked_mul(60_000_000)?)?
26129        .checked_add(sec_us)?;
26130    Some(if neg { -total } else { total })
26131}
26132
26133/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26134/// every spelling PG accepts (measured against live PG18.4, not guessed):
26135/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26136/// Before this, the unit table matched long names only, with an ad-hoc
26137/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26138/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26139/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26140/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26141/// fractional) both read from this one table now.
26142fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26143    let u = raw.to_ascii_lowercase();
26144    Some(match u.as_str() {
26145        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26146            "microsecond"
26147        }
26148        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26149            "millisecond"
26150        }
26151        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26152        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26153        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26154        "day" | "days" | "d" => "day",
26155        "week" | "weeks" | "w" => "week",
26156        "month" | "months" | "mon" | "mons" => "month",
26157        "year" | "years" | "yr" | "yrs" | "y" => "year",
26158        "decade" | "decades" | "dec" | "decs" => "decade",
26159        "century" | "centuries" | "cent" | "c" => "century",
26160        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26161        _ => return None,
26162    })
26163}
26164
26165/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26166/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26168pub(crate) enum IntervalField {
26169    Year,
26170    Month,
26171    Day,
26172    Hour,
26173    Minute,
26174    Second,
26175}
26176
26177/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26178/// spellings aren't standard for the qualifier position, so only the singular
26179/// forms are accepted.
26180/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26181/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26182/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26183/// take a `'1 2'` style literal — are not read here; they stay a parse
26184/// error rather than being silently misread.)
26185/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26186///
26187/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26188/// to do with a `@@` engine setting, and an unset one reads NULL rather
26189/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26190/// were the same node and `SELECT @x` answered "Unknown system variable".)
26191/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26192/// not see a session override — measured, after `SET autocommit=0`,
26193/// `@@global.autocommit` is still 1.
26194///
26195/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26196/// the parser's nesting budget is tuned against, and building these
26197/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26198/// wall `parse_left_right_atom` and friends were factored out for).
26199#[inline(never)]
26200fn variable_ref_atom(raw: &str) -> Expr {
26201    let user_var = !raw.starts_with("@@");
26202    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26203    Expr::FunctionCall {
26204        name: String::from(if user_var {
26205            "__spg_user_var"
26206        } else {
26207            "__spg_session_var"
26208        }),
26209        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26210    }
26211}
26212
26213fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26214    let Token::Ident(s) = tok else { return None };
26215    Some(match () {
26216        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26217        () if s.eq_ignore_ascii_case("second") => "second",
26218        () if s.eq_ignore_ascii_case("minute") => "minute",
26219        () if s.eq_ignore_ascii_case("hour") => "hour",
26220        () if s.eq_ignore_ascii_case("day") => "day",
26221        () if s.eq_ignore_ascii_case("week") => "week",
26222        () if s.eq_ignore_ascii_case("month") => "month",
26223        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26224        () if s.eq_ignore_ascii_case("year") => "year",
26225        () => return None,
26226    })
26227}
26228
26229/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26230/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26231/// which constructs the value at run time. Only the slot the unit names
26232/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26233/// slot the builtin has (months and fractional seconds respectively).
26234fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26235    let zero = || Expr::Literal(Literal::Integer(0));
26236    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26237        lhs: alloc::boxed::Box::new(qty.clone()),
26238        op,
26239        rhs: alloc::boxed::Box::new(by),
26240    };
26241    // (years, months, weeks, days, hours, mins, secs)
26242    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26243    match unit {
26244        "year" => args[0] = qty,
26245        "quarter" => {
26246            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26247        }
26248        "month" => args[1] = qty,
26249        "week" => args[2] = qty,
26250        "day" => args[3] = qty,
26251        "hour" => args[4] = qty,
26252        "minute" => args[5] = qty,
26253        "second" => args[6] = qty,
26254        // The builtin's seconds slot takes a fraction, so microseconds ride
26255        // it scaled down; the divisor is a NUMERIC literal so the division
26256        // stays exact rather than going through a float.
26257        "microsecond" => {
26258            args[6] = scaled(
26259                crate::ast::BinOp::Div,
26260                Expr::Literal(Literal::Numeric {
26261                    unscaled: 1_000_000,
26262                    scale: 0,
26263                }),
26264            );
26265        }
26266        _ => args[3] = qty,
26267    }
26268    Expr::FunctionCall {
26269        name: alloc::string::String::from("make_interval"),
26270        args,
26271    }
26272}
26273
26274/// `(count, unit)` → `(months, days, micros)`.
26275fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26276    let n: i64 = count.trim().parse().ok()?;
26277    Some(match unit {
26278        "microsecond" => (0, 0, n),
26279        "second" => (0, 0, n.checked_mul(1_000_000)?),
26280        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26281        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26282        "day" => (0, i32::try_from(n).ok()?, 0),
26283        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26284        "month" => (i32::try_from(n).ok()?, 0, 0),
26285        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26286        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26287        _ => return None,
26288    })
26289}
26290
26291fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26292    let Token::Ident(s) = tok else { return None };
26293    Some(match () {
26294        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26295        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26296        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26297        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26298        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26299        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26300        () => return None,
26301    })
26302}
26303
26304/// v7.39 (read01 round 102) — interpret an interval literal under a field
26305/// qualifier. Returns `(months, days, micros)`.
26306///
26307/// * A single field applied to a bare number sets which unit the number means,
26308///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26309///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26310/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26311/// * Every other range, and any literal a single field can't read as a plain
26312///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26313///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26314///   like PG, and the qualifier there only bounds precision.
26315fn interpret_qualified_interval(
26316    text: &str,
26317    (f1, f2): (IntervalField, Option<IntervalField>),
26318) -> Option<(i32, i32, i64)> {
26319    if let Some(f2) = f2 {
26320        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26321            if let Some(m) = parse_year_month_literal(text) {
26322                return Some((m, 0, 0));
26323            }
26324        }
26325        return parse_interval_text(text);
26326    }
26327    // Single field: reinterpret a bare number; otherwise the default parse.
26328    let trimmed = text.trim();
26329    if let Ok(val) = trimmed.parse::<f64>() {
26330        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26331        #[allow(clippy::cast_possible_truncation)]
26332        let whole = val as i64;
26333        #[allow(clippy::cast_possible_truncation)]
26334        let secs_micros = {
26335            let m = val * 1_000_000.0;
26336            if m >= 0.0 {
26337                (m + 0.5) as i64
26338            } else {
26339                (m - 0.5) as i64
26340            }
26341        };
26342        return Some(match f1 {
26343            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26344            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26345            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26346            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26347            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26348            IntervalField::Second => (0, 0, secs_micros),
26349        });
26350    }
26351    parse_interval_text(text)
26352}
26353
26354/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26355fn parse_year_month_literal(text: &str) -> Option<i32> {
26356    let t = text.trim();
26357    let (neg, body) = match t.strip_prefix('-') {
26358        Some(r) => (true, r),
26359        None => (false, t.strip_prefix('+').unwrap_or(t)),
26360    };
26361    let mut it = body.split('-');
26362    let years: i32 = it.next()?.trim().parse().ok()?;
26363    let months: i32 = match it.next() {
26364        Some(m) => m.trim().parse().ok()?,
26365        None => 0,
26366    };
26367    if it.next().is_some() {
26368        return None;
26369    }
26370    let total = years.checked_mul(12)?.checked_add(months)?;
26371    Some(if neg { -total } else { total })
26372}
26373
26374pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26375    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26376    // `@` is decorative; a trailing `ago` negates the whole interval.
26377    let mut trimmed = s.trim();
26378    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26379    let mut negate = false;
26380    if let Some(rest) = trimmed
26381        .strip_suffix("ago")
26382        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26383    {
26384        negate = true;
26385        trimmed = rest.trim();
26386    }
26387    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26388        let (mo, d, us) = v?;
26389        if negate {
26390            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26391        } else {
26392            Some((mo, d, us))
26393        }
26394    };
26395    let s = trimmed;
26396    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26397    // are single tokens, not the `<n> <unit>` pair form handled below.
26398    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26399        return finish(parse_iso8601_interval(rest));
26400    }
26401    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26402        if let Some(iv) = parse_year_month_interval(trimmed) {
26403            return finish(Some(iv));
26404        }
26405    }
26406    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26407    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26408    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26409    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26410        if let Ok(n) = trimmed.parse::<i64>() {
26411            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26412        }
26413        if let Ok(f) = trimmed.parse::<f64>() {
26414            if f.is_finite() {
26415                #[allow(clippy::cast_possible_truncation)]
26416                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26417            }
26418        }
26419    }
26420    // v7.39 (round 243) — PG accepts the number and unit run together
26421    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26422    // the `<n> <unit>` pair loop below sees them as two.
26423    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26424    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26425    for p in raw_parts {
26426        let boundary = p
26427            .char_indices()
26428            .find(|(i, c)| {
26429                *i > 0
26430                    && c.is_ascii_alphabetic()
26431                    && p[..*i]
26432                        .chars()
26433                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26434                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26435            })
26436            .map(|(i, _)| i);
26437        match boundary {
26438            Some(i) => {
26439                parts.push(&p[..i]);
26440                parts.push(&p[i..]);
26441            }
26442            None => parts.push(p),
26443        }
26444    }
26445    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26446    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26447    // remains is the `<n> <unit>` pair form handled below.
26448    let mut clock_us: i64 = 0;
26449    let mut had_clock = false;
26450    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26451        clock_us = parse_interval_clock(parts[pos])?;
26452        parts.remove(pos);
26453        had_clock = true;
26454    }
26455    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26456    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26457    let mut lone_days: i32 = 0;
26458    if had_clock && parts.len() == 1 {
26459        if let Ok(n) = parts[0].parse::<i64>() {
26460            lone_days = i32::try_from(n).ok()?;
26461            parts.clear();
26462        }
26463    }
26464    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26465        return None;
26466    }
26467    let mut months: i32 = 0;
26468    let mut days: i32 = lone_days;
26469    let mut micros: i64 = clock_us;
26470    let mut i = 0;
26471    while i < parts.len() {
26472        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26473        if let Ok(n) = parts[i].parse::<i64>() {
26474            match unit_stripped {
26475                "microsecond" => micros = micros.checked_add(n)?,
26476                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26477                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26478                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26479                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26480                "day" => {
26481                    let n32 = i32::try_from(n).ok()?;
26482                    days = days.checked_add(n32)?;
26483                }
26484                "week" => {
26485                    let n32 = i32::try_from(n).ok()?;
26486                    days = days.checked_add(n32.checked_mul(7)?)?;
26487                }
26488                "month" => {
26489                    let n32 = i32::try_from(n).ok()?;
26490                    months = months.checked_add(n32)?;
26491                }
26492                "year" => {
26493                    let n32 = i32::try_from(n).ok()?;
26494                    months = months.checked_add(n32.checked_mul(12)?)?;
26495                }
26496                // v7.39 (read01 timestamp.c) — the larger calendar units.
26497                "decade" => {
26498                    let n32 = i32::try_from(n).ok()?;
26499                    months = months.checked_add(n32.checked_mul(120)?)?;
26500                }
26501                "century" => {
26502                    let n32 = i32::try_from(n).ok()?;
26503                    months = months.checked_add(n32.checked_mul(1200)?)?;
26504                }
26505                "millennium" => {
26506                    let n32 = i32::try_from(n).ok()?;
26507                    months = months.checked_add(n32.checked_mul(12000)?)?;
26508                }
26509                _ => return None,
26510            }
26511        } else if let Ok(f) = parts[i].parse::<f64>() {
26512            // Fractional units cascade down to the next-finer field the way
26513            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26514            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26515            // no_std: f64 has no trunc/fract/round methods, so do them with
26516            // casts (toward-zero) + explicit round-half-away-from-zero.
26517            #[allow(clippy::cast_possible_truncation)]
26518            fn round_i64(x: f64) -> i64 {
26519                if x >= 0.0 {
26520                    (x + 0.5) as i64
26521                } else {
26522                    (x - 0.5) as i64
26523                }
26524            }
26525            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26526            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26527                const DAY_US: f64 = 86_400_000_000.0;
26528                let whole = d as i64; // truncates toward zero
26529                let frac = d - whole as f64;
26530                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26531                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26532                Some(())
26533            }
26534            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26535            match unit_stripped {
26536                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26537                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26538                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26539                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26540                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26541                "day" => add_days_frac(&mut days, &mut micros, f)?,
26542                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26543                "month" => {
26544                    let whole = f as i64;
26545                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26546                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26547                }
26548                "year" => {
26549                    let m = f * 12.0;
26550                    let whole = m as i64;
26551                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26552                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26553                }
26554                _ => return None,
26555            }
26556        } else {
26557            return None;
26558        }
26559        i += 2;
26560    }
26561    finish(Some((months, days, micros)))
26562}
26563
26564/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26565/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26566/// `interval` is intentionally absent (handled by its own parser arm).
26567/// Returns `None` for names that aren't sensible as a bare typed literal, so
26568/// the caller falls back to treating the ident as a column reference.
26569fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26570    Some(match ident {
26571        "date" => CastTarget::Date,
26572        "timestamp" | "datetime" => CastTarget::Timestamp,
26573        "timestamptz" => CastTarget::Timestamptz,
26574        "bool" | "boolean" => CastTarget::Bool,
26575        "int" | "integer" | "int4" => CastTarget::Int,
26576        "bigint" | "int8" => CastTarget::BigInt,
26577        "float8" | "double precision" => CastTarget::Float,
26578        "uuid" => CastTarget::Uuid,
26579        "bytea" => CastTarget::Bytea,
26580        "json" => CastTarget::Json,
26581        "jsonb" => CastTarget::Jsonb,
26582        // Types without a dedicated CastTarget variant flow through the
26583        // generic Named path (engine resolves via column_type_to_data_type).
26584        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26585        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26586        | "money" | "bit" | "varbit"
26587        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26588        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26589        // Range / multirange types likewise.
26590        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26591        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26592        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26593        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26594        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26595            CastTarget::Named(alloc::string::String::from(ident))
26596        }
26597        _ => return None,
26598    })
26599}
26600
26601/// v7.12.4 — map a bare type-name identifier (the form that
26602/// appears in a function arg list or RETURNS clause) to a
26603/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26604/// types so the caller can preserve them as
26605/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26606///
26607/// Subset of the full column-type grammar — we deliberately
26608/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26609/// here because function-arg types in v7.12.4 are mostly the
26610/// bare form (`text`, `int`, `bytea`, …).
26611/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26612/// than being `name TYPE`?
26613///
26614/// The multi-word spellings SQL allows for a bare argument type, each
26615/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26616///
26617/// NOTE this list also exists in `spg-storage`, which computes the
26618/// signature key from the rendered argument text and has to reach the
26619/// same verdict. The two crates are siblings — neither depends on the
26620/// other — and each already carries its own table of type spellings
26621/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26622/// there), so this follows the structure rather than inventing new
26623/// duplication. Recorded as V49.
26624pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26625    let t = phrase.trim().to_ascii_lowercase();
26626    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26627    matches!(
26628        base,
26629        "double precision"
26630            | "character varying"
26631            | "bit varying"
26632            | "timestamp with time zone"
26633            | "timestamp without time zone"
26634            | "time with time zone"
26635            | "time without time zone"
26636            | "national character"
26637            | "national character varying"
26638    )
26639}
26640
26641fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26642    Some(match ident.to_ascii_lowercase().as_str() {
26643        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26644        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26645        "bigint" => ColumnTypeName::BigInt,
26646        "float" | "double" => ColumnTypeName::Float,
26647        // v7.39 (round 269) — real is 32-bit.
26648        "real" | "float4" => ColumnTypeName::Real,
26649        "text" => ColumnTypeName::Text,
26650        "bool" | "boolean" => ColumnTypeName::Bool,
26651        "date" => ColumnTypeName::Date,
26652        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26653        "timestamptz" => ColumnTypeName::Timestamptz,
26654        "json" => ColumnTypeName::Json,
26655        "jsonb" => ColumnTypeName::Jsonb,
26656        "bytea" | "bytes" => ColumnTypeName::Bytes,
26657        "tsvector" => ColumnTypeName::TsVector,
26658        "tsquery" => ColumnTypeName::TsQuery,
26659        "uuid" => ColumnTypeName::Uuid,
26660        "interval" => ColumnTypeName::Interval,
26661        "time" => ColumnTypeName::Time,
26662        "year" => ColumnTypeName::Year,
26663        "timetz" => ColumnTypeName::TimeTz,
26664        "money" => ColumnTypeName::Money,
26665        _ => return None,
26666    })
26667}
26668
26669/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26670/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26671///
26672/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26673/// / embedded SQL land in v7.12.5+):
26674///
26675/// ```text
26676///   body          := [ws] block [ws]
26677///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26678///   stmt          := assign | return
26679///   assign        := assign_target := expr
26680///   assign_target := ( NEW | OLD ) . ident | ident
26681///   return        := RETURN ( NEW | OLD | NULL | expr )
26682/// ```
26683///
26684/// `expr` is parsed by recursing into the regular `Parser` — so a
26685/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26686/// NEW.subject || ' ' || NEW.sender)` body shape works without
26687/// the body parser knowing what `to_tsvector` is.
26688///
26689/// Errors here cause the caller to fall back to
26690/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26691/// successful, but the executor will refuse to invoke the
26692/// function with an "unparseable body" error.
26693/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26694/// from the crate root as `spg_sql::parse_function_body`.
26695pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26696    parse_plpgsql_body(body)
26697}
26698
26699fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26700    // Use the regular lexer on the body text. The trailing
26701    // `END;` may or may not have a semicolon; the lexer treats
26702    // both forms identically.
26703    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26704        message: alloc::format!("plpgsql body lex error: {e}"),
26705        token_pos: 0,
26706    })?;
26707    let mut parser = Parser::new(tokens);
26708    parser.parse_plpgsql_block()
26709}
26710
26711/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26712fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26713    match v {
26714        crate::ast::SetValue::String(s)
26715        | crate::ast::SetValue::Ident(s)
26716        | crate::ast::SetValue::Number(s) => s.clone(),
26717        crate::ast::SetValue::Default => "DEFAULT".into(),
26718    }
26719}
26720
26721/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26722/// contains an aggregate call at ITS OWN query level (recursion stops at
26723/// sublink boundaries — a sublink's aggregates belong to the sublink).
26724/// Backs the "aggregate functions are not allowed in a recursive query's
26725/// recursive term" well-formedness check.
26726fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26727    const AGG_NAMES: &[&str] = &[
26728        "count",
26729        "sum",
26730        "min",
26731        "max",
26732        "avg",
26733        "string_agg",
26734        "array_agg",
26735        "bool_and",
26736        "bool_or",
26737        "every",
26738        "any_value",
26739        "json_agg",
26740        "jsonb_agg",
26741        "json_object_agg",
26742        "jsonb_object_agg",
26743        "bit_and",
26744        "bit_or",
26745        "bit_xor",
26746        "var_pop",
26747        "var_samp",
26748        "variance",
26749        "stddev",
26750        "stddev_pop",
26751        "stddev_samp",
26752        "range_agg",
26753        "range_intersect_agg",
26754        "percentile_cont",
26755        "percentile_disc",
26756        "mode",
26757        "corr",
26758        "covar_pop",
26759        "covar_samp",
26760    ];
26761    match e {
26762        Expr::AggregateOrdered { .. } => true,
26763        Expr::FunctionCall { name, args } => {
26764            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26765                || args.iter().any(expr_has_toplevel_aggregate)
26766        }
26767        Expr::NamedArg { expr, .. }
26768        | Expr::Variadic(expr)
26769        | Expr::Unary { expr, .. }
26770        | Expr::Cast { expr, .. }
26771        | Expr::IsNull { expr, .. }
26772        | Expr::FieldAccess { base: expr, .. }
26773        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26774        Expr::Binary { lhs, rhs, .. } => {
26775            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26776        }
26777        Expr::Like { expr, pattern, .. } => {
26778            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26779        }
26780        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26781        Expr::InList { expr, list, .. } => {
26782            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26783        }
26784        Expr::ArraySubscript { target, index } => {
26785            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26786        }
26787        Expr::ArraySlice { target, lo, hi } => {
26788            expr_has_toplevel_aggregate(target)
26789                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26790                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26791        }
26792        Expr::AnyAll { expr, array, .. } => {
26793            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26794        }
26795        Expr::Case {
26796            operand,
26797            branches,
26798            else_branch,
26799        } => {
26800            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26801                || branches
26802                    .iter()
26803                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26804                || else_branch
26805                    .as_deref()
26806                    .is_some_and(expr_has_toplevel_aggregate)
26807        }
26808        // The outer-level operands of a sublink can aggregate; the sublink's
26809        // own body cannot leak its aggregates up here.
26810        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26811        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26812            row.iter().any(expr_has_toplevel_aggregate)
26813        }
26814        _ => false,
26815    }
26816}
26817
26818/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26819/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26820/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26821/// sublink and is legal in a recursive term, so it is not walked here.
26822fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26823    let mut exprs: Vec<&Expr> = Vec::new();
26824    for it in &s.items {
26825        if let crate::ast::SelectItem::Expr { expr, .. } = it {
26826            exprs.push(expr);
26827        }
26828    }
26829    if let Some(w) = &s.where_ {
26830        exprs.push(w);
26831    }
26832    if let Some(h) = &s.having {
26833        exprs.push(h);
26834    }
26835    if let Some(g) = &s.group_by {
26836        exprs.extend(g.iter());
26837    }
26838    if let Some(from) = &s.from {
26839        for j in &from.joins {
26840            if let Some(on) = &j.on {
26841                exprs.push(on);
26842            }
26843        }
26844    }
26845    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26846}
26847
26848/// Does this expression contain a sublink whose subquery mentions `name`?
26849fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26850    match e {
26851        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26852        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26853        Expr::InSubquery { expr, subquery, .. } => {
26854            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26855        }
26856        Expr::RowInSubquery { row, subquery, .. } => {
26857            row.iter().any(|x| expr_sublink_mentions(x, name))
26858                || select_mentions_table(subquery, name)
26859        }
26860        Expr::RowCmpSubquery { row, subquery, .. } => {
26861            row.iter().any(|x| expr_sublink_mentions(x, name))
26862                || select_mentions_table(subquery, name)
26863        }
26864        Expr::NamedArg { expr, .. }
26865        | Expr::Variadic(expr)
26866        | Expr::Unary { expr, .. }
26867        | Expr::Cast { expr, .. }
26868        | Expr::IsNull { expr, .. }
26869        | Expr::FieldAccess { base: expr, .. }
26870        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26871        Expr::Binary { lhs, rhs, .. } => {
26872            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26873        }
26874        Expr::Like { expr, pattern, .. } => {
26875            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26876        }
26877        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26878            args.iter().any(|x| expr_sublink_mentions(x, name))
26879        }
26880        Expr::InList { expr, list, .. } => {
26881            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26882        }
26883        Expr::ArraySubscript { target, index } => {
26884            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26885        }
26886        Expr::ArraySlice { target, lo, hi } => {
26887            expr_sublink_mentions(target, name)
26888                || lo
26889                    .as_deref()
26890                    .is_some_and(|x| expr_sublink_mentions(x, name))
26891                || hi
26892                    .as_deref()
26893                    .is_some_and(|x| expr_sublink_mentions(x, name))
26894        }
26895        Expr::AnyAll { expr, array, .. } => {
26896            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26897        }
26898        Expr::Case {
26899            operand,
26900            branches,
26901            else_branch,
26902        } => {
26903            operand
26904                .as_deref()
26905                .is_some_and(|x| expr_sublink_mentions(x, name))
26906                || branches
26907                    .iter()
26908                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26909                || else_branch
26910                    .as_deref()
26911                    .is_some_and(|x| expr_sublink_mentions(x, name))
26912        }
26913        _ => false,
26914    }
26915}
26916
26917/// Does this SELECT (in full — FROM tables, derived tables, its own
26918/// sublinks, and union arms) mention the named table?
26919fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26920    if let Some(from) = &s.from {
26921        if from.primary.name.eq_ignore_ascii_case(name) {
26922            return true;
26923        }
26924        if let Some(sub) = &from.primary.lateral_subquery
26925            && select_mentions_table(sub, name)
26926        {
26927            return true;
26928        }
26929        for j in &from.joins {
26930            if j.table.name.eq_ignore_ascii_case(name) {
26931                return true;
26932            }
26933            if let Some(sub) = &j.table.lateral_subquery
26934                && select_mentions_table(sub, name)
26935            {
26936                return true;
26937            }
26938        }
26939    }
26940    if select_has_self_ref_in_sublink(s, name) {
26941        return true;
26942    }
26943    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26944}
26945
26946/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26947/// row count, the way PG evaluates one before applying it.
26948///
26949/// `None` = not a constant (a column, a subquery, a function call).
26950/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26951/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26952/// All wordings were read off live PG 18.4.
26953fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26954    use crate::ast::{BinOp, Expr, Literal, UnOp};
26955    match e {
26956        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26957        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26958            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26959        }
26960        // PG coerces a string by its CONTENT, and fails on the value.
26961        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26962            |_| {
26963                Err(alloc::format!(
26964                    "invalid input syntax for type bigint: \"{t}\""
26965                ))
26966            },
26967            |n| Ok(i128::from(n)),
26968        )),
26969        Expr::Literal(Literal::Bool(_)) => Some(Err(
26970            "argument of {L} must be type bigint, not type boolean".into(),
26971        )),
26972        Expr::Unary {
26973            op: UnOp::Neg,
26974            expr,
26975        } => match fold_limit_constant(expr)? {
26976            Ok(v) => Some(Ok(-v)),
26977            e @ Err(_) => Some(e),
26978        },
26979        Expr::Binary { lhs, op, rhs } => {
26980            let a = match fold_limit_constant(lhs)? {
26981                Ok(v) => v,
26982                e @ Err(_) => return Some(e),
26983            };
26984            let b = match fold_limit_constant(rhs)? {
26985                Ok(v) => v,
26986                e @ Err(_) => return Some(e),
26987            };
26988            let out = match op {
26989                BinOp::Add => a.checked_add(b),
26990                BinOp::Sub => a.checked_sub(b),
26991                BinOp::Mul => a.checked_mul(b),
26992                BinOp::Div if b != 0 => a.checked_div(b),
26993                BinOp::Div => return Some(Err("division by zero".into())),
26994                BinOp::Mod if b != 0 => a.checked_rem(b),
26995                BinOp::Mod => return Some(Err("division by zero".into())),
26996                _ => return None,
26997            };
26998            // PG evaluates the arithmetic in the operand's own type, so an
26999            // int-by-int product that leaves int range fails there — before
27000            // the row count is ever looked at.
27001            match out {
27002                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27003                    Some(Err("integer out of range".into()))
27004                }
27005                Some(v) => Some(Ok(v)),
27006                None => Some(Err("integer out of range".into())),
27007            }
27008        }
27009        _ => None,
27010    }
27011}
27012
27013/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27014/// cast, which is what makes `LIMIT 2.5` keep three rows.
27015fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27016    if scale == 0 {
27017        return unscaled;
27018    }
27019    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27020        return 0;
27021    };
27022    let neg = unscaled < 0;
27023    let mag = unscaled.unsigned_abs() as i128;
27024    let rounded = (mag + div / 2) / div;
27025    if neg { -rounded } else { rounded }
27026}
27027
27028#[cfg(test)]
27029mod tests {
27030    use super::*;
27031    use alloc::string::ToString;
27032
27033    fn parse(s: &str) -> Statement {
27034        parse_statement(s).expect("parse ok")
27035    }
27036
27037    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27038    // `tables`, `partition`, etc. are unreserved keywords per PG's
27039    // `pg_get_keywords()` and MUST be usable as column / table /
27040    // alias names. Pre-T4 every drop-in user whose schema had one
27041    // of these as a column name (sentori events.release, mailrs
27042    // messages.index in some forks) blew the parser up at CREATE
27043    // TABLE time with "expected identifier, got Release". The
27044    // generalisation lives in `unreserved_keyword_text` + the
27045    // `expect_ident_like` and `parse_atom` arms that consult it.
27046    #[test]
27047    fn release_usable_as_column_name_in_create_table() {
27048        let stmt =
27049            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27050        if let Statement::CreateTable(t) = stmt {
27051            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27052            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27053        } else {
27054            panic!("expected CreateTable");
27055        }
27056    }
27057
27058    #[test]
27059    fn release_usable_as_column_ref_in_select_projection() {
27060        // The sentori `0003_partition_events.sql` INSERT-SELECT
27061        // walk references `release` in both column lists; the
27062        // projection-side use exercises `parse_atom`'s relaxed
27063        // identifier set.
27064        parse("SELECT id, release, payload FROM events WHERE id = 1");
27065    }
27066
27067    #[test]
27068    fn release_usable_as_column_ref_in_insert_column_list() {
27069        // INSERT INTO t (id, release, payload) VALUES (…)
27070        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27071    }
27072
27073    #[test]
27074    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27075        // Sentori `0013_audit_tombstone.sql` issues
27076        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27077        // emits Token::Drop (not Ident("drop")); the parser must
27078        // accept both in the ALTER COLUMN sub-dispatch.
27079        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27080    }
27081
27082    #[test]
27083    fn create_index_accepts_parenthesised_expression_key() {
27084        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27085        // expression index. Pre-T4 the parser bailed at the
27086        // inner `(` with "expected column ident or expression,
27087        // got LParen". The Token::LParen arm in CREATE INDEX
27088        // routes through the expression parser instead.
27089        parse(
27090            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27091             ON events ((payload->'bundle'->>'id'))",
27092        );
27093    }
27094
27095    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27096    // surface as parse errors, never stack overflows (embed hosts
27097    // abort on overflow).
27098    /// The nesting budget is a COUNT; what it has to fit inside is a
27099    /// number of BYTES, and only one of those two is stable across
27100    /// compiler versions. Round 847 measured 30,336 bytes per level
27101    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27102    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27103    /// aborted instead of erroring, which is precisely the outcome it
27104    /// exists to rule out.
27105    ///
27106    /// So the budget is metered rather than assumed. The ceiling leaves
27107    /// the depth SPG advertises fitting in a default 2 MiB thread with
27108    /// room to spare, in the debug build, where frames are widest.
27109    #[test]
27110    fn nesting_frame_cost_stays_under_ceiling() {
27111        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27112        // thread keeps a margin for whatever called the parser.
27113        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27114
27115        frame_meter::reset();
27116        let depth = frame_meter::SAMPLE_HI + 8;
27117        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27118        parse(&sql);
27119
27120        let per_level = frame_meter::bytes_per_level();
27121        {
27122            extern crate std;
27123            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27124        }
27125        assert!(
27126            per_level <= CEILING,
27127            "{per_level} bytes per nesting level exceeds {CEILING}; \
27128             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27129             in parse_expr_inner / parse_unary rather than lowering the \
27130             depth or widening the stack.",
27131            per_level * MAX_NEST_DEPTH
27132        );
27133    }
27134
27135    #[test]
27136    fn nesting_budget_errors_cleanly() {
27137        let depth = MAX_NEST_DEPTH + 50;
27138        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27139        let err = parse_statement(&sql).expect_err("must reject");
27140        assert!(err.message.contains("nests deeper"), "{err:?}");
27141        // Within budget still parses.
27142        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27143        parse(&sql);
27144    }
27145
27146    #[test]
27147    fn binary_chain_budget_errors_cleanly() {
27148        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27149        let err = parse_statement(&sql).expect_err("must reject");
27150        assert!(err.message.contains("chained binary"), "{err:?}");
27151        // Within budget still parses (chain depth ≤ budget is safe
27152        // for recursive eval/drop on 2 MiB stacks).
27153        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27154        parse(&sql);
27155    }
27156
27157    #[test]
27158    fn in_list_unaffected_by_chain_budget() {
27159        // Flat InList: 20k elements parse fine and stay flat.
27160        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27161        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27162        let Statement::Select(s) = parse(&sql) else {
27163            panic!("expected select")
27164        };
27165        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27166            panic!("expected flat InList, got {:?}", s.where_)
27167        };
27168        assert_eq!(list.len(), 20_000);
27169        assert!(!negated);
27170    }
27171
27172    fn lit_int(n: i64) -> Expr {
27173        Expr::Literal(Literal::Integer(n))
27174    }
27175
27176    fn col(name: &str) -> Expr {
27177        Expr::Column(ColumnName {
27178            qualifier: None,
27179            name: name.into(),
27180        })
27181    }
27182
27183    #[test]
27184    fn select_single_integer() {
27185        let s = parse("SELECT 1");
27186        let Statement::Select(s) = s else {
27187            panic!("expected SELECT")
27188        };
27189        assert_eq!(s.items.len(), 1);
27190        assert!(s.from.is_none());
27191        assert!(s.where_.is_none());
27192    }
27193
27194    #[test]
27195    fn select_multiple_literal_kinds() {
27196        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27197        let Statement::Select(s) = s else {
27198            panic!("expected SELECT")
27199        };
27200        assert_eq!(s.items.len(), 5);
27201    }
27202
27203    #[test]
27204    fn select_wildcard_from_table() {
27205        let s = parse("SELECT * FROM users");
27206        let Statement::Select(s) = s else {
27207            panic!("expected SELECT")
27208        };
27209        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27210        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27211    }
27212
27213    #[test]
27214    fn select_with_table_alias() {
27215        let s = parse("SELECT * FROM users AS u");
27216        let Statement::Select(s) = s else {
27217            panic!("expected SELECT")
27218        };
27219        let t = &s.from.as_ref().unwrap().primary;
27220        assert_eq!(t.name, "users");
27221        assert_eq!(t.alias.as_deref(), Some("u"));
27222    }
27223
27224    #[test]
27225    fn select_with_where_eq() {
27226        let s = parse("SELECT a FROM t WHERE a = 1");
27227        let Statement::Select(s) = s else {
27228            panic!("expected SELECT")
27229        };
27230        let w = s.where_.unwrap();
27231        assert_eq!(
27232            w,
27233            Expr::Binary {
27234                lhs: Box::new(col("a")),
27235                op: BinOp::Eq,
27236                rhs: Box::new(lit_int(1)),
27237            }
27238        );
27239    }
27240
27241    #[test]
27242    fn arithmetic_precedence() {
27243        let s = parse("SELECT 1 + 2 * 3");
27244        let Statement::Select(s) = s else {
27245            panic!("expected SELECT")
27246        };
27247        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27248            panic!("wildcard?")
27249        };
27250        assert_eq!(
27251            expr,
27252            &Expr::Binary {
27253                lhs: Box::new(lit_int(1)),
27254                op: BinOp::Add,
27255                rhs: Box::new(Expr::Binary {
27256                    lhs: Box::new(lit_int(2)),
27257                    op: BinOp::Mul,
27258                    rhs: Box::new(lit_int(3)),
27259                }),
27260            }
27261        );
27262    }
27263
27264    #[test]
27265    fn parentheses_override_precedence() {
27266        let s = parse("SELECT (1 + 2) * 3");
27267        let Statement::Select(s) = s else {
27268            panic!("expected SELECT")
27269        };
27270        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27271            panic!()
27272        };
27273        assert_eq!(
27274            expr,
27275            &Expr::Binary {
27276                lhs: Box::new(Expr::Binary {
27277                    lhs: Box::new(lit_int(1)),
27278                    op: BinOp::Add,
27279                    rhs: Box::new(lit_int(2)),
27280                }),
27281                op: BinOp::Mul,
27282                rhs: Box::new(lit_int(3)),
27283            }
27284        );
27285    }
27286
27287    #[test]
27288    fn not_binds_below_comparison() {
27289        // `NOT a = 1` should parse as `NOT (a = 1)`.
27290        let s = parse("SELECT NOT a = 1 FROM t");
27291        let Statement::Select(s) = s else {
27292            panic!("expected SELECT")
27293        };
27294        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27295            panic!()
27296        };
27297        assert_eq!(
27298            expr,
27299            &Expr::Unary {
27300                op: UnOp::Not,
27301                expr: Box::new(Expr::Binary {
27302                    lhs: Box::new(col("a")),
27303                    op: BinOp::Eq,
27304                    rhs: Box::new(lit_int(1)),
27305                }),
27306            }
27307        );
27308    }
27309
27310    #[test]
27311    fn unary_minus_binds_above_multiplication() {
27312        // `-a * 2` should be `(-a) * 2`.
27313        let s = parse("SELECT -a * 2 FROM t");
27314        let Statement::Select(s) = s else {
27315            panic!("expected SELECT")
27316        };
27317        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27318            panic!()
27319        };
27320        assert_eq!(
27321            expr,
27322            &Expr::Binary {
27323                lhs: Box::new(Expr::Unary {
27324                    op: UnOp::Neg,
27325                    expr: Box::new(col("a")),
27326                }),
27327                op: BinOp::Mul,
27328                rhs: Box::new(lit_int(2)),
27329            }
27330        );
27331    }
27332
27333    #[test]
27334    fn qualified_column() {
27335        let s = parse("SELECT t.col FROM t");
27336        let Statement::Select(s) = s else {
27337            panic!("expected SELECT")
27338        };
27339        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27340            panic!()
27341        };
27342        assert_eq!(
27343            expr,
27344            &Expr::Column(ColumnName {
27345                qualifier: Some("t".into()),
27346                name: "col".into()
27347            })
27348        );
27349    }
27350
27351    #[test]
27352    fn select_item_alias_with_as() {
27353        let s = parse("SELECT a AS y FROM t");
27354        let Statement::Select(s) = s else {
27355            panic!("expected SELECT")
27356        };
27357        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27358            panic!()
27359        };
27360        assert_eq!(alias.as_deref(), Some("y"));
27361    }
27362
27363    #[test]
27364    fn trailing_semicolon_accepted() {
27365        let s = parse("SELECT 1;");
27366        let Statement::Select(s) = s else {
27367            panic!("expected SELECT")
27368        };
27369        assert_eq!(s.items.len(), 1);
27370    }
27371
27372    #[test]
27373    fn boolean_chain_with_and_or_not() {
27374        // (NOT a) OR (b AND (NOT c))
27375        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27376        let Statement::Select(s) = s else {
27377            panic!("expected SELECT")
27378        };
27379        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27380            panic!()
27381        };
27382        let expected = Expr::Binary {
27383            lhs: Box::new(Expr::Unary {
27384                op: UnOp::Not,
27385                expr: Box::new(col("a")),
27386            }),
27387            op: BinOp::Or,
27388            rhs: Box::new(Expr::Binary {
27389                lhs: Box::new(col("b")),
27390                op: BinOp::And,
27391                rhs: Box::new(Expr::Unary {
27392                    op: UnOp::Not,
27393                    expr: Box::new(col("c")),
27394                }),
27395            }),
27396        };
27397        assert_eq!(expr, &expected);
27398    }
27399
27400    #[test]
27401    fn empty_input_errors() {
27402        // v7.14.0 — pg_dump preambles emit several comment-only
27403        // / blank-line statements that collapse to Statement::
27404        // Empty rather than a parse error. The old "SELECT in
27405        // message" assertion is stale; verify the new contract:
27406        // empty / whitespace / comment-only input parses to
27407        // Statement::Empty.
27408        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27409        assert!(matches!(
27410            parse_statement("  \n\t ").unwrap(),
27411            Statement::Empty
27412        ));
27413        // Sanity: malformed-but-non-empty still errors.
27414        assert!(parse_statement("SELECT FROM WHERE").is_err());
27415    }
27416
27417    #[test]
27418    fn unmatched_paren_errors() {
27419        assert!(parse_statement("SELECT (1 + 2").is_err());
27420    }
27421
27422    #[test]
27423    fn display_round_trip_simple_select() {
27424        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27425        let text = original.to_string();
27426        let again = parse_statement(&text).expect("re-parse");
27427        assert_eq!(original, again);
27428    }
27429
27430    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27431
27432    #[test]
27433    fn create_table_single_column() {
27434        let s = parse("CREATE TABLE foo (a INT)");
27435        let Statement::CreateTable(c) = s else {
27436            panic!("expected CreateTable")
27437        };
27438        assert_eq!(c.name, "foo");
27439        assert_eq!(c.columns.len(), 1);
27440        assert_eq!(c.columns[0].name, "a");
27441        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27442        assert!(c.columns[0].nullable);
27443    }
27444
27445    #[test]
27446    fn create_table_multi_column_with_not_null_mix() {
27447        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27448        let Statement::CreateTable(c) = s else {
27449            panic!()
27450        };
27451        assert_eq!(c.columns.len(), 4);
27452        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27453        assert!(!c.columns[0].nullable);
27454        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27455        assert!(c.columns[1].nullable);
27456        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27457        assert!(!c.columns[2].nullable);
27458        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27459    }
27460
27461    #[test]
27462    fn create_table_bigint_supported() {
27463        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27464        let Statement::CreateTable(c) = s else {
27465            panic!()
27466        };
27467        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27468    }
27469
27470    #[test]
27471    fn create_table_vector_default_is_f32() {
27472        let s = parse("CREATE TABLE t (v VECTOR(128))");
27473        let Statement::CreateTable(c) = s else {
27474            panic!()
27475        };
27476        assert_eq!(
27477            c.columns[0].ty,
27478            ColumnTypeName::Vector {
27479                dim: 128,
27480                encoding: VecEncoding::F32,
27481            },
27482        );
27483    }
27484
27485    #[test]
27486    fn create_table_vector_using_sq8() {
27487        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27488        // Case-insensitive on both `USING` and the encoding name.
27489        for sql in [
27490            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27491            "CREATE TABLE t (v VECTOR(128) using sq8)",
27492        ] {
27493            let s = parse(sql);
27494            let Statement::CreateTable(c) = s else {
27495                panic!()
27496            };
27497            assert_eq!(
27498                c.columns[0].ty,
27499                ColumnTypeName::Vector {
27500                    dim: 128,
27501                    encoding: VecEncoding::Sq8,
27502                },
27503                "{sql}",
27504            );
27505        }
27506    }
27507
27508    #[test]
27509    fn create_table_vector_using_unknown_errors() {
27510        // v7.16.1 — the inline `USING <encoding>` shape on
27511        // CREATE TABLE column defs was withdrawn before
27512        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27513        // (col vector_<metric>_ops)`; the parser now rejects
27514        // USING at column-list position with a clearer
27515        // "expected ',' or ')'" message. Test asserts the
27516        // current rejection, not the old "unknown vector
27517        // encoding" string.
27518        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27519        assert!(
27520            err.message.contains("USING")
27521                || err.message.contains("using")
27522                || err.message.contains("')'")
27523                || err.message.contains("','"),
27524            "expected USING/column-list rejection, got: {}",
27525            err.message
27526        );
27527    }
27528
27529    #[test]
27530    fn vector_using_sq8_display_roundtrips() {
27531        // The Display impl must produce text that re-parses to the
27532        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27533        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27534        let Statement::CreateTable(c) = s else {
27535            panic!()
27536        };
27537        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27538    }
27539
27540    #[test]
27541    fn parser_recognises_placeholders() {
27542        use crate::ast::{Expr, SelectItem, Statement};
27543        // $N in expression position parses as Expr::Placeholder(N).
27544        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27545        let Statement::Select(sel) = s else { panic!() };
27546        assert!(matches!(
27547            sel.items[0],
27548            SelectItem::Expr {
27549                expr: Expr::Placeholder(1),
27550                alias: None
27551            }
27552        ));
27553        // $2 + 1
27554        let SelectItem::Expr {
27555            expr: Expr::Binary { lhs, rhs, .. },
27556            ..
27557        } = &sel.items[1]
27558        else {
27559            panic!()
27560        };
27561        assert!(matches!(**lhs, Expr::Placeholder(2)));
27562        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27563        // WHERE x = $3
27564        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27565            panic!()
27566        };
27567        assert!(matches!(**rhs, Expr::Placeholder(3)));
27568    }
27569
27570    #[test]
27571    fn parser_rejects_dollar_zero() {
27572        // $0 is not valid in PG; the lexer rejects it.
27573        assert!(parse_statement("SELECT $0").is_err());
27574    }
27575
27576    #[test]
27577    fn placeholder_display_roundtrips() {
27578        // The Display impl must produce text that re-lexes to the
27579        // same Placeholder token.
27580        let s = parse("SELECT $42 FROM t");
27581        let printed = s.to_string();
27582        assert!(printed.contains("$42"));
27583        let again = parse(&printed);
27584        assert_eq!(s, again);
27585    }
27586
27587    #[test]
27588    fn alter_index_rebuild_bare() {
27589        use crate::ast::{AlterIndexTarget, Statement};
27590        let s = parse("ALTER INDEX my_idx REBUILD");
27591        let Statement::AlterIndex(a) = s else {
27592            panic!("expected AlterIndex, got {s:?}")
27593        };
27594        assert_eq!(a.name, "my_idx");
27595        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27596    }
27597
27598    #[test]
27599    fn alter_index_rebuild_with_encoding() {
27600        use crate::ast::{AlterIndexTarget, Statement};
27601        for (sql, want) in [
27602            (
27603                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27604                VecEncoding::F32,
27605            ),
27606            (
27607                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27608                VecEncoding::Sq8,
27609            ),
27610            (
27611                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27612                VecEncoding::F16,
27613            ),
27614        ] {
27615            let s = parse(sql);
27616            let Statement::AlterIndex(a) = s else {
27617                panic!("{sql}: expected AlterIndex")
27618            };
27619            assert_eq!(a.name, "my_idx");
27620            assert_eq!(
27621                a.target,
27622                AlterIndexTarget::Rebuild {
27623                    encoding: Some(want)
27624                },
27625                "{sql}"
27626            );
27627        }
27628    }
27629
27630    #[test]
27631    fn alter_index_rebuild_unknown_encoding_errors() {
27632        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27633        assert!(
27634            err.message.contains("unknown vector encoding"),
27635            "got: {}",
27636            err.message
27637        );
27638    }
27639
27640    #[test]
27641    fn alter_index_rebuild_display_roundtrips() {
27642        for (input, want) in [
27643            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27644            (
27645                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27646                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27647            ),
27648            (
27649                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27650                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27651            ),
27652        ] {
27653            let s = parse(input);
27654            assert_eq!(s.to_string(), want);
27655        }
27656    }
27657
27658    #[test]
27659    fn create_table_unknown_type_defers_to_engine() {
27660        // v4.9 picked XML as a parse-time "unsupported column
27661        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27662        // an unknown type ident parses as Text + `user_type_ref`
27663        // so CREATE TABLE can resolve user-defined enum / domain
27664        // types — rejection of truly-unknown types moved to the
27665        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27666        // to a first-class built-in, so this probe switched to a
27667        // synthetic name nothing in the lexer will ever recognise.
27668        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27669        let Statement::CreateTable(t) = stmt else {
27670            panic!("expected CreateTable");
27671        };
27672        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27673    }
27674
27675    #[test]
27676    fn create_table_missing_table_keyword_errors() {
27677        assert!(parse_statement("CREATE x (a INT)").is_err());
27678    }
27679
27680    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27681    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27682
27683    #[test]
27684    fn parse_create_table_partition_by_range() {
27685        use crate::ast::{PartitionBySpec, PartitionKindAst};
27686        let stmt = parse_statement(
27687            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27688             payload JSONB) PARTITION BY RANGE (ts)",
27689        )
27690        .unwrap();
27691        let Statement::CreateTable(t) = stmt else {
27692            panic!("expected CreateTable");
27693        };
27694        assert!(t.partition_of.is_none(), "parent has no partition_of");
27695        assert_eq!(t.columns.len(), 3);
27696        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27697        assert_eq!(
27698            by,
27699            &PartitionBySpec {
27700                kind: PartitionKindAst::Range,
27701                key_columns: alloc::vec!["ts".to_string()],
27702            }
27703        );
27704        // Display round-trip preserves the suffix. `quote_ident`
27705        // only adds double quotes when the ident needs escaping, so
27706        // a plain `ts` survives bare here.
27707        assert!(
27708            t.to_string().contains("PARTITION BY RANGE (ts)"),
27709            "Display lost PARTITION BY suffix: {t}"
27710        );
27711    }
27712
27713    #[test]
27714    fn parse_create_table_partition_of_range() {
27715        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27716        let stmt = parse_statement(
27717            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27718             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27719        )
27720        .unwrap();
27721        let Statement::CreateTable(t) = stmt else {
27722            panic!("expected CreateTable");
27723        };
27724        assert!(t.columns.is_empty(), "child inherits columns from parent");
27725        assert!(t.partition_by.is_none());
27726        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27727        assert_eq!(of.parent_name, "events_partitioned");
27728        let PartitionOfSpec { bounds, .. } = of.clone();
27729        match bounds {
27730            PartitionOfBoundsAst::Range { lower, upper } => {
27731                assert!(lower.to_string().contains("2026-06-01"));
27732                assert!(upper.to_string().contains("2026-07-01"));
27733            }
27734            other => panic!("expected Range, got {other:?}"),
27735        }
27736        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27737        // skips quotes when not required, so the parent name appears
27738        // bare here.
27739        let s = t.to_string();
27740        assert!(
27741            s.contains("PARTITION OF events_partitioned"),
27742            "Display lost PARTITION OF: {s}"
27743        );
27744        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27745        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27746    }
27747
27748    #[test]
27749    fn parse_create_table_partition_of_default() {
27750        use crate::ast::PartitionOfBoundsAst;
27751        let stmt =
27752            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27753                .unwrap();
27754        let Statement::CreateTable(t) = stmt else {
27755            panic!("expected CreateTable");
27756        };
27757        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27758        assert_eq!(of.parent_name, "events_partitioned");
27759        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27760        assert!(
27761            t.to_string()
27762                .contains("PARTITION OF events_partitioned DEFAULT"),
27763            "Display lost DEFAULT: {t}"
27764        );
27765    }
27766
27767    #[test]
27768    fn parse_create_table_partition_by_list() {
27769        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27770        // child with `FOR VALUES IN (lit, lit, …)`.
27771        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27772        let parent =
27773            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27774                .unwrap();
27775        let Statement::CreateTable(t) = parent else {
27776            panic!("expected CreateTable");
27777        };
27778        let Some(PartitionBySpec {
27779            kind,
27780            ref key_columns,
27781        }) = t.partition_by
27782        else {
27783            panic!("expected PARTITION BY");
27784        };
27785        assert_eq!(kind, PartitionKindAst::List);
27786        assert_eq!(*key_columns, vec!["region".to_string()]);
27787        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27788
27789        let child = parse_statement(
27790            "CREATE TABLE events_apac PARTITION OF events_listed \
27791             FOR VALUES IN ('jp', 'kr', 'tw')",
27792        )
27793        .unwrap();
27794        let Statement::CreateTable(c) = child else {
27795            panic!("expected CreateTable");
27796        };
27797        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27798        let PartitionOfBoundsAst::List { values } = &of.bounds else {
27799            panic!("expected List bounds, got {:?}", of.bounds);
27800        };
27801        assert_eq!(values.len(), 3);
27802        let disp = c.to_string();
27803        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27804    }
27805
27806    #[test]
27807    fn parse_create_table_partition_by_hash() {
27808        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27809        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27810        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27811        let parent =
27812            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27813        let Statement::CreateTable(t) = parent else {
27814            panic!("expected CreateTable");
27815        };
27816        let Some(PartitionBySpec {
27817            kind,
27818            ref key_columns,
27819        }) = t.partition_by
27820        else {
27821            panic!("expected PARTITION BY");
27822        };
27823        assert_eq!(kind, PartitionKindAst::Hash);
27824        assert_eq!(*key_columns, vec!["id".to_string()]);
27825        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27826
27827        let child = parse_statement(
27828            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27829             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27830        )
27831        .unwrap();
27832        let Statement::CreateTable(c) = child else {
27833            panic!("expected CreateTable");
27834        };
27835        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27836        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27837            panic!("expected Hash bounds");
27838        };
27839        assert_eq!(modulus, 4);
27840        assert_eq!(remainder, 0);
27841        let disp = c.to_string();
27842        assert!(
27843            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27844            "Display lost HASH bounds: {disp}"
27845        );
27846
27847        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27848        let bad = parse_statement(
27849            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27850             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27851        );
27852        let msg = format!("{}", bad.unwrap_err());
27853        assert!(
27854            msg.contains("REMAINDER") && msg.contains("MODULUS"),
27855            "expected REMAINDER/MODULUS validation error: {msg}"
27856        );
27857    }
27858
27859    #[test]
27860    fn parse_create_table_partition_of_rejects_columns() {
27861        // v7.37.6-B contract: PARTITION OF children inherit columns
27862        // from the parent; an explicit list MUST surface as a parse
27863        // error rather than getting silently ignored.
27864        let err = parse_statement(
27865            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27866             FOR VALUES FROM ('a') TO ('b')",
27867        );
27868        assert!(err.is_err(), "expected parse error for explicit columns");
27869        let msg = format!("{}", err.unwrap_err());
27870        assert!(
27871            msg.contains("PARTITION OF") && msg.contains("column"),
27872            "error should mention PARTITION OF + columns: {msg}"
27873        );
27874    }
27875
27876    #[test]
27877    fn insert_single_value() {
27878        let s = parse("INSERT INTO foo VALUES (42)");
27879        let Statement::Insert(i) = s else {
27880            panic!("expected Insert")
27881        };
27882        assert_eq!(i.table, "foo");
27883        assert_eq!(i.rows.len(), 1);
27884        assert_eq!(i.rows[0].len(), 1);
27885        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27886    }
27887
27888    #[test]
27889    fn insert_multi_value_with_mixed_literals() {
27890        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27891        let Statement::Insert(i) = s else { panic!() };
27892        assert_eq!(i.rows.len(), 1);
27893        assert_eq!(i.rows[0].len(), 5);
27894    }
27895
27896    #[test]
27897    fn insert_missing_into_errors() {
27898        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27899    }
27900
27901    #[test]
27902    fn create_table_round_trip() {
27903        let original =
27904            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27905        let text = original.to_string();
27906        let again = parse_statement(&text).expect("re-parse");
27907        assert_eq!(original, again);
27908    }
27909
27910    #[test]
27911    fn insert_round_trip_with_negation_and_string() {
27912        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27913        let text = original.to_string();
27914        let again = parse_statement(&text).expect("re-parse");
27915        assert_eq!(original, again);
27916    }
27917
27918    #[test]
27919    fn unknown_keyword_at_statement_start_errors() {
27920        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27921        // the top-level dispatch still has no branch to take.
27922        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27923        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27924    }
27925
27926    // --- v0.8 CREATE INDEX --------------------------------------------------
27927
27928    #[test]
27929    fn create_index_basic() {
27930        let s = parse("CREATE INDEX idx_id ON users (id)");
27931        let Statement::CreateIndex(c) = s else {
27932            panic!("expected CreateIndex")
27933        };
27934        assert_eq!(c.name, "idx_id");
27935        assert_eq!(c.table, "users");
27936        assert_eq!(c.column, "id");
27937    }
27938
27939    #[test]
27940    fn create_index_missing_on_errors() {
27941        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27942    }
27943
27944    #[test]
27945    fn create_index_missing_paren_errors() {
27946        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27947    }
27948
27949    #[test]
27950    fn create_index_round_trip() {
27951        let original = parse("CREATE INDEX by_name ON users (name)");
27952        let again = parse_statement(&original.to_string()).unwrap();
27953        assert_eq!(original, again);
27954    }
27955
27956    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27957
27958    #[test]
27959    fn create_unique_index_basic() {
27960        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27961        let Statement::CreateIndex(c) = s else {
27962            panic!("expected CreateIndex");
27963        };
27964        assert!(c.is_unique);
27965        assert_eq!(c.column, "a");
27966        assert!(c.partial_predicate.is_none());
27967    }
27968
27969    #[test]
27970    fn create_unique_index_partial() {
27971        // mailrs's email_templates "one default per user" shape.
27972        let s = parse(
27973            "CREATE UNIQUE INDEX idx_email_templates_user_default \
27974             ON email_templates (user_address) WHERE is_default = true",
27975        );
27976        let Statement::CreateIndex(c) = s else {
27977            panic!("expected CreateIndex");
27978        };
27979        assert!(c.is_unique);
27980        assert_eq!(c.table, "email_templates");
27981        assert_eq!(c.column, "user_address");
27982        assert!(c.partial_predicate.is_some());
27983    }
27984
27985    #[test]
27986    fn create_unique_index_composite_with_predicate() {
27987        // mailrs's calendar_events instance: composite columns.
27988        let s = parse(
27989            "CREATE UNIQUE INDEX uq_calendar_events_instance \
27990             ON calendar_events (calendar_id, uid, recurrence_id) \
27991             WHERE recurrence_id IS NOT NULL",
27992        );
27993        let Statement::CreateIndex(c) = s else {
27994            panic!("expected CreateIndex");
27995        };
27996        assert!(c.is_unique);
27997        assert_eq!(c.column, "calendar_id");
27998        assert_eq!(
27999            c.extra_columns,
28000            vec!["uid".to_string(), "recurrence_id".to_string()]
28001        );
28002        assert!(c.partial_predicate.is_some());
28003    }
28004
28005    #[test]
28006    fn create_unique_index_using_btree_ok() {
28007        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28008        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28009    }
28010
28011    #[test]
28012    fn create_unique_index_using_hnsw_rejected() {
28013        let err =
28014            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28015        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28016    }
28017
28018    #[test]
28019    fn create_unique_index_round_trip() {
28020        let original = parse(
28021            "CREATE UNIQUE INDEX uq_calendar_events_master \
28022             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28023        );
28024        let again = parse_statement(&original.to_string()).unwrap();
28025        assert_eq!(original, again);
28026    }
28027
28028    #[test]
28029    fn create_unique_without_index_errors() {
28030        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28031        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28032        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28033    }
28034
28035    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28036
28037    #[test]
28038    fn create_table_bytea_column() {
28039        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28040        let Statement::CreateTable(c) = s else {
28041            panic!("expected CreateTable");
28042        };
28043        assert_eq!(c.columns.len(), 2);
28044        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28045        assert!(!c.columns[1].nullable);
28046    }
28047
28048    #[test]
28049    fn create_table_bytes_alias_column() {
28050        let s = parse("CREATE TABLE t (blob BYTES)");
28051        let Statement::CreateTable(c) = s else {
28052            panic!("expected CreateTable");
28053        };
28054        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28055    }
28056
28057    #[test]
28058    fn bytea_round_trip_display() {
28059        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28060        let again = parse_statement(&original.to_string()).unwrap();
28061        assert_eq!(original, again);
28062    }
28063
28064    // --- v0.9 transactions -------------------------------------------------
28065
28066    #[test]
28067    fn begin_commit_rollback_parse_as_unit_variants() {
28068        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28069        assert_eq!(parse("COMMIT"), Statement::Commit);
28070        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28071        // Trailing semicolons accepted too.
28072        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28073        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28074        // statement (with or without the WORK/TRANSACTION noise word).
28075        assert_eq!(
28076            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28077            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28078        );
28079        assert_eq!(
28080            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28081            Statement::Begin(Some(IsolationLevel::Serializable))
28082        );
28083        // A non-isolation mode keeps the session default (None).
28084        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28085    }
28086
28087    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28088
28089    #[test]
28090    fn inner_product_binop_parses() {
28091        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28092        let Statement::Select(s) = s else { panic!() };
28093        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28094            panic!()
28095        };
28096        assert!(matches!(
28097            expr,
28098            Expr::Binary {
28099                op: BinOp::InnerProduct,
28100                ..
28101            }
28102        ));
28103    }
28104
28105    #[test]
28106    fn cosine_distance_binop_parses() {
28107        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28108        let Statement::Select(s) = s else { panic!() };
28109        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28110            panic!()
28111        };
28112        assert!(matches!(
28113            expr,
28114            Expr::Binary {
28115                op: BinOp::CosineDistance,
28116                ..
28117            }
28118        ));
28119    }
28120
28121    #[test]
28122    fn vector_cast_postfix_wraps_string_literal() {
28123        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28124        let Statement::Select(s) = s else { panic!() };
28125        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28126            panic!()
28127        };
28128        assert!(matches!(
28129            expr,
28130            Expr::Cast {
28131                target: CastTarget::Vector,
28132                ..
28133            }
28134        ));
28135    }
28136
28137    #[test]
28138    fn unsupported_cast_target_errors() {
28139        // v7.37.5 ship triage promoted the parser to accept every
28140        // ident as a `CastTarget::Named(canonical)`; the engine
28141        // surfaces the "unsupported cast target" error at eval
28142        // time when `type_name_to_data_type` can't resolve it.
28143        // Parser-side error now requires a NON-ident after `::`
28144        // (e.g. a punctuation token).
28145        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28146        assert_eq!(err.message, "syntax error at or near \",\"");
28147    }
28148
28149    #[test]
28150    fn tx_statements_round_trip() {
28151        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28152            let original = parse(q);
28153            let again = parse_statement(&original.to_string()).unwrap();
28154            assert_eq!(original, again);
28155        }
28156    }
28157
28158    #[test]
28159    fn interval_text_parsing_units() {
28160        // v7.37.5 β — three-field shape `(months, days, micros)` so
28161        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28162        // Single unit.
28163        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28164        assert_eq!(
28165            parse_interval_text("24 hours"),
28166            Some((0, 0, 86_400_000_000))
28167        );
28168        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28169        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28170        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28171        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28172        // Compound spans accumulate per-dimension.
28173        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28174        assert_eq!(
28175            parse_interval_text("1 day 2 hours"),
28176            Some((0, 1, 7_200_000_000))
28177        );
28178        // Negative numbers carry through per-dimension.
28179        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28180        // Bad shapes return None.
28181        assert_eq!(parse_interval_text(""), None);
28182        assert_eq!(parse_interval_text("garbage"), None);
28183        assert_eq!(parse_interval_text("1 fortnight"), None);
28184        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28185        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28186        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28187        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28188        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28189    }
28190
28191    #[test]
28192    fn interval_literal_roundtrips_via_display() {
28193        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28194        let s = parsed.to_string();
28195        // Display preserves the original text verbatim.
28196        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28197        // And re-parsing yields a structurally equal statement.
28198        let again = parse_statement(&s).unwrap();
28199        assert_eq!(parsed, again);
28200    }
28201
28202    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28203
28204    #[test]
28205    fn parser_recognises_create_publication_bare() {
28206        let s = parse("CREATE PUBLICATION pub_a");
28207        let Statement::CreatePublication(p) = s else {
28208            panic!("expected CreatePublication, got {s:?}")
28209        };
28210        assert_eq!(p.name, "pub_a");
28211        assert_eq!(p.scope, PublicationScope::AllTables);
28212    }
28213
28214    #[test]
28215    fn parser_recognises_create_publication_for_all_tables() {
28216        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28217        let Statement::CreatePublication(p) = s else {
28218            panic!("expected CreatePublication, got {s:?}")
28219        };
28220        assert_eq!(p.name, "pub_a");
28221        assert_eq!(p.scope, PublicationScope::AllTables);
28222    }
28223
28224    #[test]
28225    fn parser_recognises_drop_publication() {
28226        let s = parse("DROP PUBLICATION pub_a");
28227        let Statement::DropPublication { name, .. } = s else {
28228            panic!("expected DropPublication, got {s:?}")
28229        };
28230        assert_eq!(name, "pub_a");
28231    }
28232
28233    #[test]
28234    fn parser_recognises_for_table_list() {
28235        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28236        let Statement::CreatePublication(p) = s else {
28237            panic!("expected CreatePublication, got {s:?}")
28238        };
28239        assert_eq!(p.name, "pub_a");
28240        let PublicationScope::ForTables(ts) = p.scope else {
28241            panic!("expected ForTables scope")
28242        };
28243        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28244    }
28245
28246    #[test]
28247    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28248        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28249        // is rejected (`invalid publication object list`; the old
28250        // test pinned an unverifiable "PG 19 accepts both" claim);
28251        // TABLES pairs with IN SCHEMA.
28252        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28253            .expect_err("bare FOR TABLES must reject");
28254        assert!(
28255            alloc::format!("{err}").contains("invalid publication object list"),
28256            "got: {err}"
28257        );
28258        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28259        let Statement::CreatePublication(p) = s else {
28260            panic!("expected CreatePublication, got {s:?}")
28261        };
28262        let PublicationScope::TablesInSchema(schema) = p.scope else {
28263            panic!("expected TablesInSchema")
28264        };
28265        assert_eq!(schema, "public");
28266    }
28267
28268    #[test]
28269    fn parser_recognises_for_all_tables_except_list() {
28270        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28271        let Statement::CreatePublication(p) = s else {
28272            panic!()
28273        };
28274        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28275            panic!("expected AllTablesExcept")
28276        };
28277        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28278    }
28279
28280    #[test]
28281    fn parser_rejects_for_table_with_empty_list() {
28282        // `FOR TABLE` with nothing after is a parse error.
28283        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28284            .expect_err("must error on empty list");
28285        // No specific message asserted — the call falls through to
28286        // expect_ident_like which yields "expected identifier, got …".
28287        assert!(!err.message.is_empty());
28288    }
28289
28290    #[test]
28291    fn parser_recognises_show_publications() {
28292        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28293        // bare ident in this position, NOT a reserved keyword.
28294        let s = parse("SHOW PUBLICATIONS");
28295        assert!(matches!(s, Statement::ShowPublications));
28296    }
28297
28298    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28299
28300    #[test]
28301    fn parser_recognises_create_subscription_single_publication() {
28302        let s = parse(
28303            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28304        );
28305        let Statement::CreateSubscription(c) = s else {
28306            panic!("expected CreateSubscription, got {s:?}")
28307        };
28308        assert_eq!(c.name, "sub_a");
28309        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28310        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28311    }
28312
28313    #[test]
28314    fn parser_recognises_create_subscription_multi_publication() {
28315        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28316        let Statement::CreateSubscription(c) = s else {
28317            panic!()
28318        };
28319        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28320    }
28321
28322    #[test]
28323    fn parser_rejects_create_subscription_missing_connection() {
28324        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28325            .expect_err("must error on missing CONNECTION");
28326        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28327    }
28328
28329    #[test]
28330    fn parser_rejects_create_subscription_missing_publication() {
28331        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28332            .expect_err("must error on missing PUBLICATION");
28333        assert_eq!(err.message, "syntax error at end of input");
28334    }
28335
28336    #[test]
28337    fn parser_recognises_drop_subscription() {
28338        let s = parse("DROP SUBSCRIPTION sub_a");
28339        let Statement::DropSubscription { name, .. } = s else {
28340            panic!("expected DropSubscription, got {s:?}")
28341        };
28342        assert_eq!(name, "sub_a");
28343    }
28344
28345    #[test]
28346    fn parser_recognises_show_subscriptions() {
28347        let s = parse("SHOW SUBSCRIPTIONS");
28348        assert!(matches!(s, Statement::ShowSubscriptions));
28349    }
28350
28351    #[test]
28352    fn parser_recognises_wait_for_wal_position_no_timeout() {
28353        let s = parse("WAIT FOR WAL POSITION 12345");
28354        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28355            panic!("expected WaitForWalPosition, got {s:?}")
28356        };
28357        assert_eq!(pos, 12345);
28358        assert!(timeout_ms.is_none());
28359    }
28360
28361    #[test]
28362    fn parser_recognises_wait_for_wal_position_with_timeout() {
28363        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28364        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28365            panic!()
28366        };
28367        assert_eq!(pos, 67890);
28368        assert_eq!(timeout_ms, Some(5000));
28369    }
28370
28371    #[test]
28372    fn parser_rejects_wait_with_negative_position() {
28373        // The lexer treats `-` as a token; `expect_u64_literal`
28374        // only sees the Integer that follows, so the negative
28375        // arrives as a unary-minus expression at higher levels.
28376        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28377        // parse error one way or another.
28378        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28379        assert!(!err.message.is_empty());
28380    }
28381
28382    #[test]
28383    fn parser_recognises_bare_analyze() {
28384        let s = parse("ANALYZE");
28385        assert!(matches!(s, Statement::Analyze(None)));
28386    }
28387
28388    #[test]
28389    fn parser_recognises_analyze_with_table() {
28390        let s = parse("ANALYZE users");
28391        let Statement::Analyze(Some(name)) = s else {
28392            panic!("expected Analyze, got {s:?}")
28393        };
28394        assert_eq!(name, "users");
28395    }
28396
28397    #[test]
28398    fn parser_recognises_analyze_with_quoted_table() {
28399        let s = parse("ANALYZE \"Mixed Case\"");
28400        let Statement::Analyze(Some(name)) = s else {
28401            panic!()
28402        };
28403        assert_eq!(name, "Mixed Case");
28404    }
28405
28406    #[test]
28407    fn parser_rejects_analyze_with_garbage_token() {
28408        let err = parse_statement("ANALYZE 42").expect_err("must error");
28409        assert!(!err.message.is_empty());
28410    }
28411
28412    #[test]
28413    fn analyze_display_roundtrips() {
28414        for sql in ["ANALYZE", "ANALYZE users"] {
28415            let s = parse(sql);
28416            let printed = s.to_string();
28417            let again = parse_statement(&printed)
28418                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28419            assert_eq!(s, again);
28420        }
28421    }
28422
28423    #[test]
28424    fn wait_for_display_roundtrips() {
28425        for sql in [
28426            "WAIT FOR WAL POSITION 12345",
28427            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28428        ] {
28429            let s = parse(sql);
28430            let printed = s.to_string();
28431            let again = parse_statement(&printed)
28432                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28433            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28434        }
28435    }
28436
28437    #[test]
28438    fn subscription_ddl_display_roundtrips() {
28439        for sql in [
28440            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28441            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28442            "DROP SUBSCRIPTION sub_a",
28443            "SHOW SUBSCRIPTIONS",
28444        ] {
28445            let s = parse(sql);
28446            let printed = s.to_string();
28447            let again = parse_statement(&printed)
28448                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28449            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28450        }
28451    }
28452
28453    #[test]
28454    fn parser_drop_dispatches_user_vs_publication() {
28455        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28456        // tokenises DROP. Both targets must still parse.
28457        let s = parse("DROP USER 'alice'");
28458        let Statement::DropUser { name, .. } = s else {
28459            panic!("expected DropUser, got {s:?}")
28460        };
28461        assert_eq!(name, "alice");
28462        // And DROP PUBLICATION lands the new variant.
28463        let s = parse("DROP PUBLICATION p1");
28464        assert!(matches!(s, Statement::DropPublication { .. }));
28465    }
28466
28467    #[test]
28468    fn publication_ddl_display_roundtrips() {
28469        // Every CREATE PUBLICATION variant must Display → parse →
28470        // same AST. v6.1.3 covers all three scope shapes.
28471        for sql in [
28472            "CREATE PUBLICATION pub_a",
28473            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28474            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28475            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28476            "DROP PUBLICATION pub_a",
28477            "SHOW PUBLICATIONS",
28478        ] {
28479            let s = parse(sql);
28480            let printed = s.to_string();
28481            let again = parse_statement(&printed)
28482                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28483            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28484        }
28485    }
28486
28487    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28488
28489    #[test]
28490    fn create_function_returns_trigger_plpgsql_minimal() {
28491        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28492        let s = parse(sql);
28493        let Statement::CreateFunction(f) = s else {
28494            panic!("expected CreateFunction");
28495        };
28496        assert_eq!(f.name, "noop");
28497        assert!(!f.or_replace);
28498        assert!(f.args.is_empty());
28499        assert!(matches!(f.returns, FunctionReturn::Trigger));
28500        assert_eq!(f.language, "plpgsql");
28501        let FunctionBody::PlPgSql(block) = f.body else {
28502            panic!("expected PlPgSql body");
28503        };
28504        assert_eq!(block.statements.len(), 1);
28505        assert!(matches!(
28506            block.statements[0],
28507            PlPgSqlStmt::Return(ReturnTarget::New)
28508        ));
28509    }
28510
28511    #[test]
28512    fn create_function_or_replace_with_assignment() {
28513        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28514        // RETURN NEW.
28515        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28516BEGIN
28517  NEW.search_vector := to_tsvector('english', NEW.subject);
28518  RETURN NEW;
28519END;
28520$$";
28521        let s = parse(sql);
28522        let Statement::CreateFunction(f) = s else {
28523            panic!("expected CreateFunction");
28524        };
28525        assert!(f.or_replace);
28526        let FunctionBody::PlPgSql(block) = &f.body else {
28527            panic!("expected PlPgSql body");
28528        };
28529        assert_eq!(block.statements.len(), 2);
28530        // First statement: NEW.search_vector := to_tsvector(...)
28531        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28532            panic!("expected Assign as first stmt");
28533        };
28534        match target {
28535            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28536            other => panic!("expected NEW.col, got {other:?}"),
28537        }
28538        // Second statement: RETURN NEW
28539        assert!(matches!(
28540            block.statements[1],
28541            PlPgSqlStmt::Return(ReturnTarget::New)
28542        ));
28543    }
28544
28545    #[test]
28546    fn create_trigger_after_insert_or_update() {
28547        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28548        let s = parse(sql);
28549        let Statement::CreateTrigger(t) = s else {
28550            panic!("expected CreateTrigger");
28551        };
28552        assert_eq!(t.name, "tg");
28553        assert_eq!(t.table, "messages");
28554        assert_eq!(t.timing, TriggerTiming::After);
28555        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28556        assert_eq!(t.for_each, TriggerForEach::Row);
28557        assert_eq!(t.function, "update_sv");
28558    }
28559
28560    #[test]
28561    fn create_trigger_before_delete_execute_procedure_alias() {
28562        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28563        let sql =
28564            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28565        let s = parse(sql);
28566        let Statement::CreateTrigger(t) = s else {
28567            panic!("expected CreateTrigger");
28568        };
28569        assert_eq!(t.timing, TriggerTiming::Before);
28570        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28571    }
28572
28573    #[test]
28574    fn drop_trigger_if_exists_round_trips() {
28575        // No parser support for DROP TRIGGER yet — added in v7.12.5
28576        // alongside the broader DROP …{IF EXISTS} cleanup. The
28577        // AST + Display impls are in place so we round-trip via
28578        // construction:
28579        let s = Statement::DropTrigger {
28580            name: "tg".into(),
28581            table: "messages".into(),
28582            if_exists: true,
28583        };
28584        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28585    }
28586
28587    #[test]
28588    fn trigger_ddl_display_roundtrips_through_parser() {
28589        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28590        // Display → parse → same AST (modulo PL/pgSQL body
28591        // formatting which is parser-canonicalised).
28592        for sql in [
28593            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28594            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28595        ] {
28596            let s = parse(sql);
28597            let printed = s.to_string();
28598            let again = parse_statement(&printed)
28599                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28600            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28601        }
28602    }
28603}