Skip to main content

spg_sql/
parser.rs

1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21    AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22    CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23    CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24    ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25    FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26    FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27    GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28    NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29    PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30    Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31    WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38    Vec<Expr>,
39    Vec<(Expr, bool, Option<bool>)>,
40    Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50    if grant {
51        Statement::Grant(g)
52    } else {
53        Statement::Revoke(g)
54    }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58    matches!(
59        lc,
60        // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61        // real statement with a real store. v7.39 (read01 round 57): "grant" /
62        // "revoke" moved OUT — table privileges are now REAL (stored in
63        // `relacl`, enforced against the session role); a grant on any other
64        // object class still parses and no-ops so dumps restore.
65        // MySQL bulk-load brackets.
66        "unlock"
67            // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68            // diagnostics that pg_dump-style tools also emit
69            // post-restore.
70            | "optimize"
71            | "check"
72            | "use"
73            // PG psql backslash meta-commands that newer
74            // pg_dump versions emit unescaped (\restrict /
75            // \unrestrict). Real psql intercepts these; SPG's
76            // PG-wire sees them as raw text.
77            | "\\restrict"
78            | "\\unrestrict"
79            // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80            // `DELIMITER ;` directives. Technically client-side
81            // (the `mysql` CLI uses them to set the statement
82            // terminator), not SQL — but mysqldump and stored-
83            // procedure scripts emit them inline. SPG's parser
84            // sees one statement at a time and doesn't care
85            // about the terminator, so consume DELIMITER lines
86            // as Empty.
87            | "delimiter"
88            // v7.37.17 (17.6 siblings) — additional PG maintenance /
89            // session-state statements pg_dump + application startup
90            // scripts emit. SPG has no matching session-state to
91            // discard (no prepared-plan cache surface, no temp
92            // sequences), no matching security-label / storage-
93            // option to apply, no separate CREATE/DROP CAST that
94            // affects execution.
95            // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96            // pg_dump / pg_dumpall emit around DROP ROLE:
97            //   REASSIGN OWNED BY <role> [, ...] TO <newrole>
98            //   DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99            // Both operate on the role's owned objects; SPG has no
100            // role-owner model, so accept-and-no-op.
101            // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102            // + extension scripts use LOAD to preload shared
103            // libraries. SPG doesn't have a shared-library extension
104            // point today (extensions ship as first-class crates
105            // linked at build time); accept as a no-op.
106            | "load"
107    )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133    let s = match tok {
134        // PG keyword class: unreserved or col_name.
135        //
136        Token::Release => "release",
137        Token::Savepoint => "savepoint",
138        Token::Show => "show",
139        Token::Index => "index",
140        Token::Begin => "begin",
141        Token::Commit => "commit",
142        Token::Rollback => "rollback",
143        Token::Drop => "drop",
144        Token::Insert => "insert",
145        Token::Values => "values",
146        Token::Limit => "limit",
147        Token::Partition => "partition",
148        Token::Tables => "tables",
149        Token::Connection => "connection",
150        Token::Publication => "publication",
151        Token::Subscription => "subscription",
152        Token::Interval => "interval",
153        // `extract` is non-reserved in PG too (it's a function the
154        // parser dispatches via context — outside that context it's
155        // a plain identifier).
156        Token::Extract => "extract",
157        Token::Offset => "offset",
158        // `to` is reserved in PG (used in many "AS … TO …" forms), so
159        // it is NOT relaxed here. Same for `from`, `where`, `as`,
160        // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161        // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162        // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163        // `left`, `cross`, `outer`, `default`, `is`, `between`,
164        // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165        // (partial — keep partition as unreserved per modern PG).
166        _ => return None,
167    };
168    Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183    s.eq_ignore_ascii_case("jsonb_each_text")
184        || s.eq_ignore_ascii_case("jsonb_each")
185        || s.eq_ignore_ascii_case("json_each_text")
186        || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195    fname: &str,
196    args: Vec<Expr>,
197    names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199    let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200        "make_date" => &["year", "month", "day"],
201        "make_time" => &["hour", "min", "sec"],
202        "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203        "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204        other => {
205            return Err(alloc::format!(
206                "function {other}(...) does not support named arguments"
207            ));
208        }
209    };
210    let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211    let mut next_positional = 0usize;
212    for (arg, name) in args.into_iter().zip(names.iter()) {
213        let idx = match name {
214            Some(n) => params
215                .iter()
216                .position(|p| p.eq_ignore_ascii_case(n))
217                .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218            None => {
219                let i = next_positional;
220                next_positional += 1;
221                i
222            }
223        };
224        if idx >= slots.len() {
225            return Err(alloc::format!("too many arguments for {fname}(...)"));
226        }
227        if slots[idx].is_some() {
228            return Err(alloc::format!(
229                "argument \"{}\" specified more than once",
230                params[idx]
231            ));
232        }
233        slots[idx] = Some(arg);
234    }
235    Ok(slots
236        .into_iter()
237        .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238        .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248    /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249    Expanded(String),
250    /// Exponent pushes the value outside PG's numeric format
251    /// (more than 131072 integer digits or 16383 fractional digits).
252    Overflow,
253    /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254    NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262    let s = s.trim();
263    let Some(epos) = s.find(['e', 'E']) else {
264        return SciExpanded::NotScientific;
265    };
266    let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267    let Ok(exp) = exp_str.parse::<i64>() else {
268        return SciExpanded::NotScientific;
269    };
270    let (neg, mant) = match mant.strip_prefix('-') {
271        Some(r) => (true, r),
272        None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273    };
274    let (int_part, frac_part) = match mant.split_once('.') {
275        Some((i, f)) => (i, f),
276        None => (mant, ""),
277    };
278    if (int_part.is_empty() && frac_part.is_empty())
279        || !int_part.bytes().all(|b| b.is_ascii_digit())
280        || !frac_part.bytes().all(|b| b.is_ascii_digit())
281    {
282        return SciExpanded::NotScientific;
283    }
284    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285    digits.push_str(int_part);
286    digits.push_str(frac_part);
287    // Decimal point position within `digits` after applying the exponent.
288    let new_point = int_part.len() as i64 + exp;
289    // PG's numeric format: up to 131072 digits before the point, 16383 after.
290    if new_point > 131_072 {
291        return SciExpanded::Overflow;
292    }
293    if (digits.len() as i64 - new_point) > 16_383 {
294        return SciExpanded::Overflow;
295    }
296    let sign = if neg { "-" } else { "" };
297    let plain = if new_point <= 0 {
298        let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299        out.push_str("0.");
300        for _ in 0..(-new_point) {
301            out.push('0');
302        }
303        out.push_str(&digits);
304        out
305    } else if (new_point as usize) >= digits.len() {
306        let mut out = digits;
307        for _ in 0..(new_point as usize - out.len()) {
308            out.push('0');
309        }
310        out
311    } else {
312        let mut out = String::with_capacity(digits.len() + 1);
313        out.push_str(&digits[..new_point as usize]);
314        out.push('.');
315        out.push_str(&digits[new_point as usize..]);
316        out
317    };
318    SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327    let padded = if hex.len() % 2 == 1 {
328        alloc::format!("0{hex}")
329    } else {
330        hex.to_string()
331    };
332    Expr::Cast {
333        expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334            "\\x{padded}"
335        )))),
336        target: CastTarget::Named("bytea".to_string()),
337    }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345    let pad = (8 - bits.len() % 8) % 8;
346    let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347    let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348    for nibble in padded.as_bytes().chunks(4) {
349        let mut v = 0u8;
350        for &b in nibble {
351            v = (v << 1) | (b - b'0');
352        }
353        hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354    }
355    hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365    match parse_decimal_literal(&s) {
366        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368        // its exact value as a NumericBig.
369        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370        // v7.39 (read01 numeric.c) — expand the exponent form.
371        None => match expand_scientific_literal(&s) {
372            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374                None if plain
375                    .split_once('.')
376                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377                {
378                    Ok(Literal::NumericBig(plain))
379                }
380                None => s
381                    .parse::<f64>()
382                    .map(Literal::Float)
383                    .map_err(|_| format!("invalid numeric literal {s:?}")),
384            },
385            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386            SciExpanded::NotScientific => s
387                .parse::<f64>()
388                .map(Literal::Float)
389                .map_err(|_| format!("invalid numeric literal {s:?}")),
390        },
391    }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395    let (int_part, frac_part) = match s.split_once('.') {
396        Some((i, f)) => (i, f),
397        None => (s, ""),
398    };
399    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400    // places fell out of the numeric path here, which is why
401    // `pg_typeof(1e-256)` answered double precision and a plain
402    // 256-place decimal aborted the query in the big-decimal converter.
403    if frac_part.len() > u16::MAX as usize {
404        return None;
405    }
406    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407    digits.push_str(int_part);
408    digits.push_str(frac_part);
409    let mantissa: i128 = digits.parse().ok()?;
410    #[allow(clippy::cast_possible_truncation)]
411    Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418    s.eq_ignore_ascii_case("jsonb_to_recordset")
419        || s.eq_ignore_ascii_case("jsonb_to_record")
420        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421        // column-definition list desugars identically (the record base
422        // argument only carries the type; a non-NULL base's field
423        // defaults are a recorded delta).
424        || s.eq_ignore_ascii_case("json_populate_record")
425        || s.eq_ignore_ascii_case("jsonb_populate_record")
426        || s.eq_ignore_ascii_case("json_populate_recordset")
427        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428        || s.eq_ignore_ascii_case("json_to_recordset")
429        || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433    /// Whether what follows an identifier ends an index key, which is how
434    /// an operator class is told from anything else in that position.
435    fn opclass_position_follows(next: Option<&Token>) -> bool {
436        match next {
437            // `ASC` / `DESC` have their own tokens; matching them as
438            // identifiers named "asc" / "desc" — which the first version of
439            // this did — never fires, and `(c text_pattern_ops DESC)` (which
440            // PG18.4 accepts, verified) went on failing to parse.
441            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442            Some(Token::Ident(w)) => {
443                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444            }
445            _ => false,
446        }
447    }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451    let lc = name.to_ascii_lowercase();
452    matches!(
453        lc.as_str(),
454        "vector_cosine_ops"
455            | "vector_l2_ops"
456            | "vector_ip_ops"
457            | "halfvec_cosine_ops"
458            | "halfvec_l2_ops"
459            | "halfvec_ip_ops"
460            | "sq8_cosine_ops"
461            | "sq8_l2_ops"
462            | "sq8_ip_ops"
463            // pg_trgm — trigram operator class. SPG's GIN index
464            // already uses tsvector tokens; trigram-style LIKE
465            // pattern matching still routes through a sequential
466            // scan, but the opclass name is accepted so PG schemas
467            // load.
468            | "gin_trgm_ops"
469            | "gist_trgm_ops"
470            // PG built-in btree opclasses occasionally appear in
471            // pg_dump output for column types with multiple
472            // sort orders (text_pattern_ops, varchar_pattern_ops,
473            // bpchar_pattern_ops).
474            | "text_pattern_ops"
475            | "varchar_pattern_ops"
476            | "bpchar_pattern_ops"
477            | "int4_ops"
478            | "int8_ops"
479            | "text_ops"
480    )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485    pub message: String,
486    /// Index into the token stream where parsing tripped. Not a byte offset.
487    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488    /// field would grow every `Result<_, ParseError>` slot on the deeply
489    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490    /// 1-based char position is recovered on the cold error path by
491    /// [`syntax_error_position`], which re-tokenizes to map this token index.
492    pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497    /// with `parse error at token #N: `, which PG has no equivalent of:
498    /// the message bodies are already PG's verbatim (`LIMIT must not be
499    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500    /// prefix was SPG's internal token index leaking into every one of
501    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502    /// character position from it for the ErrorResponse `P`.
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        f.write_str(&self.message)
505    }
506}
507
508impl From<LexError> for ParseError {
509    fn from(e: LexError) -> Self {
510        Self {
511            message: format!("lex: {e}"),
512            token_pos: 0,
513        }
514    }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523    let (tokens, offsets) =
524        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525    let mut p = Parser::new(tokens);
526    let expr = p
527        .parse_expr(0)
528        .and_then(|e| p.expect_eof().map(|()| e))
529        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530    Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536    parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544        .map_err(|e| shape_lex_error(&e, input))?;
545    // The same session flag names the dialect for both the lexer and
546    // the type mapping.
547    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548    let stmt = (|| {
549        let stmt = p.parse_one_statement()?;
550        if matches!(p.peek(), Token::Semicolon) {
551            p.advance();
552        }
553        p.expect_eof()?;
554        Ok(stmt)
555    })()
556    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557    Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575        return e;
576    }
577    let message = match offending_lexeme(input, offsets, e.token_pos) {
578        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579        None => "syntax error at end of input".into(),
580    };
581    ParseError {
582        message,
583        token_pos: e.token_pos,
584    }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595    use lexer::LexErrorKind as K;
596    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597    let message = match &e.kind {
598        K::UnterminatedString => {
599            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600        }
601        K::UnterminatedQuotedIdent => {
602            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603        }
604        K::UnterminatedBlockComment => {
605            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606        }
607        // PG has no "unknown character" error of its own — the character
608        // is skipped and the parser reports the next token. SPG stops at
609        // the character itself and names it, which is the same shape.
610        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611        // The number-literal kinds already carry PG's `at or near` form.
612        other => alloc::format!(
613            "{}",
614            lexer::LexError {
615                kind: other.clone(),
616                pos: e.pos,
617            }
618        ),
619    };
620    ParseError {
621        message,
622        token_pos: 0,
623    }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630    let start = *offsets.get(token_pos)?;
631    if start >= input.len() {
632        return None;
633    }
634    let end = offsets
635        .get(token_pos + 1)
636        .copied()
637        .unwrap_or(input.len())
638        .min(input.len());
639    let seg = input.get(start..end)?.trim();
640    if seg.is_empty() {
641        return None;
642    }
643    // A quoted literal / identifier keeps its inner spaces; anything else
644    // ends at the first whitespace (the segment runs to the NEXT token's
645    // start, which may swallow a comment).
646    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647        Some(seg)
648    } else {
649        seg.split_whitespace().next()
650    }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663    input: &str,
664    backslash_escapes: bool,
665    token_pos: usize,
666) -> Option<usize> {
667    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668    let byte_off = *offsets.get(token_pos)?;
669    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670        return None;
671    }
672    Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676    tokens: Vec<Token>,
677    pos: usize,
678    /// v7.39 (round 274) — the session's dialect, carried by the same
679    /// signal that drives string-literal escaping: `SET sql_mode` (only
680    /// MySQL clients and mysqldump preambles emit it) turns it on,
681    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682    /// it off. Needed here because the two dialects disagree about what
683    /// `REAL` means — see the type mapping below.
684    mysql_dialect: bool,
685    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686    /// mutually recursive expr/select parsers. Bounded so a deeply
687    /// nested input returns a parse error instead of overflowing
688    /// the stack (embed hosts die on overflow — it is an abort,
689    /// not a catchable error).
690    nest_depth: usize,
691    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692    /// `random() < p/100` predicate here; the enclosing SELECT
693    /// drains the list after its WHERE parses and ANDs the
694    /// predicates in. parse_bare_select save/restores around its
695    /// FROM+WHERE so nested selects only drain their own.
696    pending_sample_preds: Vec<Expr>,
697    /// v7.39 (round 691) — collation lowering channel, the same shape as
698    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699    /// information, and `ast::OrderBy` is where this parser keeps ordering
700    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701    /// variant — puts a new arm on `eval_expr`, which this repo has
702    /// measured to overflow the debug stack. So while an ORDER BY KEY is
703    /// being parsed the postfix loop drops the name here instead of
704    /// refusing it, and the key's parser takes it.
705    ///
706    /// Only inside an ORDER BY key: everywhere else an unperformable
707    /// collation still errors, because accepting one at a COMPARISON and
708    /// ignoring it is the defect F36 exists to close.
709    in_order_by_key: bool,
710    order_key_collation: Option<String>,
711    /// POSITION(sub IN str) — while parsing the needle, the IN
712    /// keyword is the argument separator, not a membership test.
713    /// The postfix loop leaves IN unconsumed when this is set.
714    suppress_in_tail: bool,
715    /// Index of the token the last `advance()` returned — see
716    /// [`Parser::consumed_pos`].
717    last_consumed: usize,
718    /// v7.39 (round 506) — the statement's own text and the byte each token
719    /// starts at, so a MySQL projection item can report the SOURCE TEXT
720    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
721    /// spacing and all. Only filled for a MySQL session — a PG one names
722    /// columns from the parsed shape and pays nothing for this.
723    src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739///     this budget and errors. Verified against a live server for nested
740///     derived tables, parens, calls, CASE, IN-subqueries, scalar
741///     subqueries, NOT and unary minus — the server stayed up through all
742///     of them. This is the contract that matters, and it holds.
743///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745///     and executing aborts around 8 inside a test thread. The budget is
746///     simply unreachable there, which is why a deep-nesting test has to
747///     ask for a large stack of its own — see `nesting_budget_errors_at`
748///     in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767    "pg_am",
768    "pg_attrdef",
769    "pg_attribute",
770    "pg_cast",
771    "pg_db_role_setting",
772    "pg_conversion",
773    "pg_default_acl",
774    "pg_shadow",
775    "pg_sequences",
776    "pg_range",
777    "pg_partitioned_table",
778    "pg_language",
779    "pg_group",
780    "pg_authid",
781    "pg_class",
782    "pg_collation",
783    "pg_constraint",
784    "pg_database",
785    "pg_depend",
786    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
787    "pg_description",
788    "pg_enum",
789    "pg_extension",
790    // v7.39 (round 541) — pg_dump reads it for every relation of kind
791    // 'f'. SPG has no foreign tables, so it is empty, which is also
792    // what PG reports on a database that has none.
793    "pg_foreign_table",
794    // v7.39 (round 541) — the empty-by-truth family; see
795    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
796    "pg_event_trigger",
797    "pg_file_settings",
798    "pg_foreign_data_wrapper",
799    "pg_foreign_server",
800    "pg_hba_file_rules",
801    "pg_ident_file_mappings",
802    "pg_init_privs",
803    "pg_parameter_acl",
804    "pg_prepared_xacts",
805    "pg_publication_namespace",
806    "pg_publication_rel",
807    "pg_publication_tables",
808    "pg_replication_origin",
809    "pg_replication_origin_status",
810    "pg_seclabel",
811    "pg_seclabels",
812    "pg_shdepend",
813    "pg_shdescription",
814    "pg_shmem_allocations",
815    "pg_shmem_allocations_numa",
816    "pg_shseclabel",
817    "pg_statistic_ext_data",
818    "pg_stats_ext",
819    "pg_stats_ext_exprs",
820    "pg_subscription_rel",
821    "pg_transform",
822    "pg_user_mapping",
823    "pg_user_mappings",
824    "pg_index",
825    "pg_indexes",
826    "pg_inherits",
827    // v7.39 (round 650) — the text-search catalogs SPG can fill
828    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
829    // token types to dictionaries and SPG has no token-type model,
830    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
831    "pg_ts_config",
832    "pg_ts_config_map",
833    "pg_ts_dict",
834    "pg_ts_parser",
835    "pg_ts_template",
836    "pg_matviews",
837    "pg_namespace",
838    // v7.39 (round 621)
839    "pg_operator",
840    "pg_policies",
841    "pg_policy",
842    "pg_proc",
843    "pg_publication",
844    "pg_replication_slots",
845    "pg_roles",
846    // v7.39 (round 143) — the rewrite-rule listing view.
847    // v7.39 (round 312) — and the rule catalogue itself, which
848    // `pg_get_ruledef(oid)` resolves against.
849    "pg_rewrite",
850    "pg_rules",
851    "pg_sequence",
852    "pg_settings",
853    "pg_stat_archiver",
854    "pg_stat_bgwriter",
855    "pg_stat_checkpointer",
856    "pg_stat_database",
857    "pg_stat_io",
858    "pg_stat_progress_analyze",
859    "pg_auth_members",
860    "pg_stat_progress_create_index",
861    "pg_stat_progress_vacuum",
862    "pg_stat_replication",
863    "pg_stat_slru",
864    "pg_stat_subscription_stats",
865    "pg_stat_user_functions",
866    "pg_stat_user_indexes",
867    "pg_stat_user_tables",
868    "pg_stat_wal",
869    "pg_prepared_statements",
870    "pg_largeobject",
871    "pg_largeobject_metadata",
872    "pg_statistic",
873    "pg_statistic_ext",
874    "pg_subscription",
875    "pg_tables",
876    "pg_tablespace",
877    // v7.39 (round 502) — the timezone catalogues. SPG resolved
878    // named zones correctly but could not list them, so a client
879    // populating a timezone picker got "relation does not exist".
880    "pg_timezone_abbrevs",
881    "pg_timezone_names",
882    "pg_trigger",
883    "pg_type",
884    "pg_user",
885    "pg_views",
886];
887
888const MAX_NEST_DEPTH: usize = 64;
889
890/// Stack accounting for the nesting budget, test-only.
891///
892/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
893/// that MOVES: a compiler upgrade grew the parser's debug frames and
894/// silently ate the margin until `nesting_budget_errors_cleanly` went
895/// from erroring cleanly to aborting on a stack overflow. A count
896/// cannot notice that on its own, so the budget is measured here and
897/// held to a ceiling.
898///
899/// The reading has to come from a helper whose OWN frame is the same at
900/// every call: debug slot placement does not follow source order, so a
901/// local's address inside the function under test is not that
902/// function's frame boundary. Two earlier probes were wrong that way —
903/// one read `&self.nest_depth`, which is the `Parser`'s address and
904/// never moves at all.
905#[cfg(test)]
906mod frame_meter {
907    extern crate std;
908    use std::cell::Cell;
909
910    // Per-THREAD, not global. `cargo test` runs tests in parallel and
911    // plenty of them parse nested expressions, so shared statics get
912    // stack addresses from several threads at once and the subtraction
913    // below turns into noise — it read 229,772 bytes per level that way,
914    // while passing when the test was run on its own.
915    std::thread_local! {
916        static AT_LO: Cell<usize> = const { Cell::new(0) };
917        static AT_HI: Cell<usize> = const { Cell::new(0) };
918    }
919
920    pub(super) const SAMPLE_LO: usize = 4;
921    pub(super) const SAMPLE_HI: usize = 24;
922
923    #[inline(never)]
924    pub(super) fn record(depth: usize) {
925        let anchor = 0u8;
926        let at = core::ptr::from_ref(&anchor) as usize;
927        if depth == SAMPLE_LO {
928            AT_LO.with(|c| c.set(at));
929        } else if depth == SAMPLE_HI {
930            AT_HI.with(|c| c.set(at));
931        }
932    }
933
934    /// Bytes of stack one nesting level costs, averaged over the span.
935    pub(super) fn bytes_per_level() -> usize {
936        let lo = AT_LO.with(Cell::get);
937        let hi = AT_HI.with(Cell::get);
938        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
939        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
940        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
941    }
942
943    pub(super) fn reset() {
944        AT_LO.with(|c| c.set(0));
945        AT_HI.with(|c| c.set(0));
946    }
947}
948
949/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
950/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
951#[inline(never)]
952fn build_center_call(e: Expr) -> Expr {
953    Expr::FunctionCall {
954        name: alloc::string::String::from("center"),
955        args: alloc::vec![e],
956    }
957}
958
959/// Max consecutive binary operators at ONE precedence level
960/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
961/// parse time but evaluates and drops recursively — depth beyond
962/// this overflows 2 MiB worker stacks (debug eval frames run
963/// multiple KiB). `IN (…)` lists are flat and unaffected.
964const MAX_BINARY_CHAIN: usize = 256;
965
966/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
967/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
968/// it keeps its dedicated path (`parse_table_level_fk`).
969enum NamedTableConstraintKind {
970    Check,
971    Unique,
972    PrimaryKey,
973    Exclude,
974}
975
976impl Parser {
977    fn new(tokens: Vec<Token>) -> Self {
978        Self::new_with_dialect(tokens, false)
979    }
980
981    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
982        Self {
983            tokens,
984            mysql_dialect,
985            in_order_by_key: false,
986            order_key_collation: None,
987            pos: 0,
988            nest_depth: 0,
989            pending_sample_preds: Vec::new(),
990            suppress_in_tail: false,
991            last_consumed: 0,
992            src: None,
993        }
994    }
995
996    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
997    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
998        if self.mysql_dialect {
999            self.src = Some((input.to_string(), offsets.to_vec()));
1000        }
1001        self
1002    }
1003
1004    /// The source text spanning tokens `start ..= end`, trimmed.
1005    ///
1006    /// The offsets are token STARTS, so the span runs to the start of the
1007    /// token after `end` and gives back the whitespace between them —
1008    /// trimming is what makes `a + b FROM t` end at `b`.
1009    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1010        let (text, offsets) = self.src.as_ref()?;
1011        let from = *offsets.get(start)?;
1012        let to = *offsets.get(end + 1)?;
1013        text.get(from..to).map(str::trim_end)
1014    }
1015
1016    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1017    /// nesting depth, erroring out cleanly past the budget.
1018    fn enter_nested(&mut self) -> Result<(), ParseError> {
1019        self.nest_depth += 1;
1020        #[cfg(test)]
1021        frame_meter::record(self.nest_depth);
1022        if self.nest_depth > MAX_NEST_DEPTH {
1023            self.nest_depth -= 1;
1024            return Err(self.err(alloc::format!(
1025                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1026            )));
1027        }
1028        Ok(())
1029    }
1030
1031    fn peek(&self) -> &Token {
1032        // tokens always ends with Eof; pos is clamped in advance().
1033        &self.tokens[self.pos]
1034    }
1035
1036    fn advance(&mut self) -> Token {
1037        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1038        self.last_consumed = self.pos;
1039        if self.pos + 1 < self.tokens.len() {
1040            self.pos += 1;
1041        }
1042        t
1043    }
1044
1045    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1046    /// returned. It was computed as `pos - 1`, which is wrong at both
1047    /// ends: `advance()` parks on the final Eof rather than running off
1048    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1049    /// input`), and after backtracking `pos` is no longer one past the
1050    /// token that failed. Recorded by `advance()` itself instead.
1051    const fn consumed_pos(&self) -> usize {
1052        self.last_consumed
1053    }
1054
1055    fn err(&self, message: String) -> ParseError {
1056        ParseError {
1057            message,
1058            token_pos: self.pos,
1059        }
1060    }
1061
1062    fn expect_eof(&self) -> Result<(), ParseError> {
1063        if matches!(self.peek(), Token::Eof) {
1064            Ok(())
1065        } else {
1066            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1067        }
1068    }
1069
1070    /// v7.14.0 — swallow every token up to (but not including) the
1071    /// next semicolon / EOF. Used by the dump-noise dispatcher
1072    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1073    /// etc. without modeling each grammar.
1074    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1075    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1076    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1077    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1078    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1079    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1080        let start = self.pos;
1081        self.advance(); // COMMENT
1082        if !matches!(self.peek(), Token::On) {
1083            self.pos = start;
1084            self.consume_until_statement_boundary();
1085            return Ok(Statement::Empty);
1086        }
1087        self.advance(); // ON
1088        let kind = match self.peek() {
1089            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1090            Token::Table => "table".into(),
1091            _ => {
1092                self.consume_until_statement_boundary();
1093                return Ok(Statement::Empty);
1094            }
1095        };
1096        if !matches!(
1097            kind.as_str(),
1098            "table"
1099                | "column"
1100                | "index"
1101                | "view"
1102                | "sequence"
1103                | "schema"
1104                | "type"
1105                | "database"
1106                | "function"
1107        ) {
1108            self.consume_until_statement_boundary();
1109            return Ok(Statement::Empty);
1110        }
1111        self.advance(); // the kind keyword
1112        // The object name. ⚠️ `expect_ident_like` strips a leading
1113        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1114        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1115        // `c`. Read the dotted parts from raw tokens instead, then let a
1116        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1117        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1118        loop {
1119            match self.advance() {
1120                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1121                other if unreserved_keyword_text(&other).is_some() => {
1122                    parts.push(unreserved_keyword_text(&other).unwrap());
1123                }
1124                other => {
1125                    return Err(ParseError {
1126                        message: alloc::format!("expected identifier, got {other:?}"),
1127                        token_pos: self.consumed_pos(),
1128                    });
1129                }
1130            }
1131            if matches!(self.peek(), Token::Dot) {
1132                self.advance();
1133            } else {
1134                break;
1135            }
1136        }
1137        // COLUMN wants `table.column`; every other kind wants a bare name.
1138        let want = if kind == "column" { 2 } else { 1 };
1139        while parts.len() > want {
1140            parts.remove(0);
1141        }
1142        let name = parts.join(".");
1143        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1144        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1145        // error here — a dump carrying one function comment failed to
1146        // restore. The list is consumed (the comment store keys by name;
1147        // overload-precise comments are the function-predicate follow-up).
1148        if matches!(self.peek(), Token::LParen)
1149            && matches!(
1150                kind.as_str(),
1151                "function" | "procedure" | "aggregate" | "routine"
1152            )
1153        {
1154            let mut depth = 0usize;
1155            loop {
1156                match self.advance() {
1157                    Token::LParen => depth += 1,
1158                    Token::RParen => {
1159                        depth -= 1;
1160                        if depth == 0 {
1161                            break;
1162                        }
1163                    }
1164                    Token::Eof => {
1165                        return Err(self.err(alloc::string::String::from(
1166                            "unterminated argument list in COMMENT ON",
1167                        )));
1168                    }
1169                    _ => {}
1170                }
1171            }
1172        }
1173        // `IS`
1174        if !matches!(self.peek(), Token::Is) {
1175            self.expect_keyword_ident("is")?;
1176        } else {
1177            self.advance();
1178        }
1179        let comment = match self.peek() {
1180            Token::Null => {
1181                self.advance();
1182                None
1183            }
1184            _ => Some(self.expect_string_literal()?),
1185        };
1186        Ok(Statement::CommentOn {
1187            kind,
1188            name,
1189            comment,
1190        })
1191    }
1192
1193    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1194    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1195    /// [CASCADE|RESTRICT]`.
1196    ///
1197    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1198    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1199    /// and the no-ON `GRANT role TO role` membership form — parses into
1200    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1201    /// on them still restores.
1202    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1203        self.advance(); // GRANT / REVOKE
1204        // REVOKE's optional `GRANT OPTION FOR` prefix.
1205        let mut grant_option = false;
1206        if !grant
1207            && self.peek_keyword_ident("grant")
1208            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1209        {
1210            self.advance(); // GRANT
1211            self.advance(); // OPTION
1212            self.expect_keyword_ident("for")?;
1213            grant_option = true;
1214        }
1215        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1216        // words each with an optional COLUMN list.
1217        let mut privileges: Vec<GrantPriv> = Vec::new();
1218        if matches!(self.peek(), Token::All) {
1219            self.advance();
1220            if self.peek_keyword_ident("privileges") {
1221                self.advance();
1222            }
1223            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1224            // column only.
1225            if matches!(self.peek(), Token::LParen) {
1226                let columns = self.parse_grant_column_list()?;
1227                privileges.push(GrantPriv {
1228                    word: "ALL".into(),
1229                    columns,
1230                });
1231            }
1232        } else {
1233            loop {
1234                // SELECT and INSERT lex as reserved tokens, so they never
1235                // reach `expect_ident_like` as plain idents; the rest
1236                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1237                // MAINTAIN) are ordinary identifiers.
1238                let w = match self.peek() {
1239                    Token::Select => {
1240                        self.advance();
1241                        "SELECT".to_string()
1242                    }
1243                    Token::Insert => {
1244                        self.advance();
1245                        "INSERT".to_string()
1246                    }
1247                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1248                    // schema / database, and it lexes as a reserved token.
1249                    Token::Create => {
1250                        self.advance();
1251                        "CREATE".to_string()
1252                    }
1253                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1254                    // alice`) these "privilege words" are ROLE NAMES, and a
1255                    // role name is case-sensitive. `priv_from_word` folds case
1256                    // itself when they really are privileges.
1257                    _ => self.expect_ident_like()?,
1258                };
1259                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1260                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1261                let columns = if matches!(self.peek(), Token::LParen) {
1262                    self.parse_grant_column_list()?
1263                } else {
1264                    Vec::new()
1265                };
1266                privileges.push(GrantPriv { word: w, columns });
1267                if matches!(self.peek(), Token::Comma) {
1268                    self.advance();
1269                } else {
1270                    break;
1271                }
1272            }
1273        }
1274        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1275        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1276        if !matches!(self.peek(), Token::On) {
1277            let roles: Vec<String> = core::mem::take(&mut privileges)
1278                .into_iter()
1279                .map(|p| p.word)
1280                .collect();
1281            let grantees = self.parse_grantee_list(grant)?;
1282            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1283            // no admin-option layer: a member cannot re-grant).
1284            self.consume_until_statement_boundary();
1285            return Ok(finish_grant(
1286                grant,
1287                GrantStatement {
1288                    privileges: Vec::new(),
1289                    object: GrantObject::Roles(roles),
1290                    grantees,
1291                    grant_option,
1292                },
1293            ));
1294        }
1295        self.advance(); // ON
1296        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1297        // the enforced case; anything else parses and no-ops.
1298        let mut class = "TABLE";
1299        match self.peek() {
1300            Token::Table => {
1301                self.advance();
1302            }
1303            Token::All => {
1304                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1305                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1306                // IN SCHEMA` stay no-ops and keep their own object class.
1307                self.advance(); // ALL
1308                let kind = match self.peek() {
1309                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1310                    // TABLES has its own token (SHOW TABLES owns it).
1311                    Token::Tables | Token::Table => "tables".to_string(),
1312                    _ => String::new(),
1313                };
1314                if !kind.is_empty() {
1315                    self.advance();
1316                }
1317                // `IN SCHEMA <name>`
1318                if matches!(self.peek(), Token::In) {
1319                    self.advance();
1320                    if self.peek_keyword_ident("schema") {
1321                        self.advance();
1322                        let _schema = self.expect_ident_like()?;
1323                    }
1324                }
1325                if kind != "tables" {
1326                    self.consume_until_statement_boundary();
1327                    return Ok(finish_grant(
1328                        grant,
1329                        GrantStatement {
1330                            privileges,
1331                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1332                            grantees: Vec::new(),
1333                            grant_option,
1334                        },
1335                    ));
1336                }
1337                let grantees = self.parse_grantee_list(grant)?;
1338                self.consume_until_statement_boundary();
1339                return Ok(finish_grant(
1340                    grant,
1341                    GrantStatement {
1342                        privileges,
1343                        object: GrantObject::AllTablesInSchema,
1344                        grantees,
1345                        grant_option,
1346                    },
1347                ));
1348            }
1349            Token::Ident(w) | Token::QuotedIdent(w) => {
1350                let lc = w.to_ascii_lowercase();
1351                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1352                // real objects with real ACLs now.
1353                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1354                    self.advance();
1355                    let mut names: Vec<String> = Vec::new();
1356                    loop {
1357                        let mut parts: Vec<String> = Vec::new();
1358                        loop {
1359                            parts.push(self.expect_ident_like()?);
1360                            if matches!(self.peek(), Token::Dot) {
1361                                self.advance();
1362                            } else {
1363                                break;
1364                            }
1365                        }
1366                        names.push(parts.pop().expect("at least one part"));
1367                        if matches!(self.peek(), Token::Comma) {
1368                            self.advance();
1369                        } else {
1370                            break;
1371                        }
1372                    }
1373                    let grantees = self.parse_grantee_list(grant)?;
1374                    let mut grant_option = grant_option;
1375                    if grant && self.peek_keyword_ident("with") {
1376                        self.advance();
1377                        self.expect_keyword_ident("grant")?;
1378                        self.expect_keyword_ident("option")?;
1379                        grant_option = true;
1380                    }
1381                    self.consume_until_statement_boundary();
1382                    let object = match lc.as_str() {
1383                        "sequence" => GrantObject::Sequences(names),
1384                        "schema" => GrantObject::Schemas(names),
1385                        _ => GrantObject::Databases(names),
1386                    };
1387                    return Ok(finish_grant(
1388                        grant,
1389                        GrantStatement {
1390                            privileges,
1391                            object,
1392                            grantees,
1393                            grant_option,
1394                        },
1395                    ));
1396                }
1397                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1398                // keys functions by NAME, so the argument list parses and is
1399                // dropped (an overload set shares one ACL — recorded residual).
1400                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1401                    self.advance();
1402                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1403                    loop {
1404                        let mut parts: Vec<String> = Vec::new();
1405                        loop {
1406                            parts.push(self.expect_ident_like()?);
1407                            if matches!(self.peek(), Token::Dot) {
1408                                self.advance();
1409                            } else {
1410                                break;
1411                            }
1412                        }
1413                        let fname = parts.pop().expect("at least one part");
1414                        // v7.39 (read01 round 62) — the signature picks the
1415                        // overload, so it is captured.
1416                        let sig = if matches!(self.peek(), Token::LParen) {
1417                            Some(self.parse_function_signature_types()?)
1418                        } else {
1419                            None
1420                        };
1421                        names.push((fname, sig));
1422                        if matches!(self.peek(), Token::Comma) {
1423                            self.advance();
1424                        } else {
1425                            break;
1426                        }
1427                    }
1428                    let grantees = self.parse_grantee_list(grant)?;
1429                    self.consume_until_statement_boundary();
1430                    return Ok(finish_grant(
1431                        grant,
1432                        GrantStatement {
1433                            privileges,
1434                            object: GrantObject::Functions(names),
1435                            grantees,
1436                            grant_option,
1437                        },
1438                    ));
1439                }
1440                if matches!(
1441                    lc.as_str(),
1442                    "type"
1443                        | "domain"
1444                        | "language"
1445                        | "tablespace"
1446                        | "large"
1447                        | "foreign"
1448                        | "parameter"
1449                ) {
1450                    self.consume_until_statement_boundary();
1451                    return Ok(finish_grant(
1452                        grant,
1453                        GrantStatement {
1454                            privileges,
1455                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1456                            grantees: Vec::new(),
1457                            grant_option,
1458                        },
1459                    ));
1460                }
1461                class = "TABLE";
1462            }
1463            _ => {}
1464        }
1465        let _ = class;
1466        // The table list. Schema-qualified names drop their qualifier (SPG is
1467        // single-schema) — but read the dotted parts from raw tokens, since
1468        // `expect_ident_like` would silently swallow the leading part.
1469        let mut tables: Vec<String> = Vec::new();
1470        loop {
1471            let mut parts: Vec<String> = Vec::new();
1472            loop {
1473                parts.push(self.expect_ident_like()?);
1474                if matches!(self.peek(), Token::Dot) {
1475                    self.advance();
1476                } else {
1477                    break;
1478                }
1479            }
1480            tables.push(parts.pop().expect("at least one part"));
1481            if matches!(self.peek(), Token::Comma) {
1482                self.advance();
1483            } else {
1484                break;
1485            }
1486        }
1487        let grantees = self.parse_grantee_list(grant)?;
1488        if grant && self.peek_keyword_ident("with") {
1489            self.advance();
1490            self.expect_keyword_ident("grant")?;
1491            self.expect_keyword_ident("option")?;
1492            grant_option = true;
1493        }
1494        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1495        // to cascade to (no re-granting), so both are accepted and ignored.
1496        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1497            self.advance();
1498        }
1499        Ok(finish_grant(
1500            grant,
1501            GrantStatement {
1502                privileges,
1503                object: GrantObject::Tables(tables),
1504                grantees,
1505                grant_option,
1506            },
1507        ))
1508    }
1509
1510    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1511    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1512    /// words; the caller normalises them into a signature key.
1513    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1514        self.advance(); // (
1515        let mut types: Vec<String> = Vec::new();
1516        if matches!(self.peek(), Token::RParen) {
1517            self.advance();
1518            return Ok(types);
1519        }
1520        loop {
1521            // Collect the words of one argument up to a comma / close paren.
1522            let mut words: Vec<String> = Vec::new();
1523            loop {
1524                match self.peek() {
1525                    Token::Comma | Token::RParen | Token::Eof => break,
1526                    _ => {}
1527                }
1528                let tok = self.advance();
1529                match tok {
1530                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1531                    other => {
1532                        if let Some(w) = unreserved_keyword_text(&other) {
1533                            words.push(w);
1534                        }
1535                    }
1536                }
1537            }
1538            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1539            // themselves several words (`double precision`, `character
1540            // varying`, `timestamp with time zone`), so "two words means the
1541            // first is a parameter name" reads the type off `f(double
1542            // precision)` as `precision`. v7.39 (round 282): recognise the
1543            // multi-word spellings first — a leading word that STARTS one of
1544            // them is part of the type, not a name.
1545            let joined = words.join(" ");
1546            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1547                joined
1548            } else if words.len() >= 2 {
1549                words[1..].join(" ")
1550            } else {
1551                words.first().cloned().unwrap_or_default()
1552            };
1553            types.push(ty);
1554            if matches!(self.peek(), Token::Comma) {
1555                self.advance();
1556            } else {
1557                break;
1558            }
1559        }
1560        if matches!(self.peek(), Token::RParen) {
1561            self.advance();
1562        }
1563        Ok(types)
1564    }
1565
1566    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1567    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1568        self.advance(); // (
1569        let mut cols = Vec::new();
1570        loop {
1571            cols.push(self.expect_ident_like()?);
1572            if matches!(self.peek(), Token::Comma) {
1573                self.advance();
1574            } else {
1575                break;
1576            }
1577        }
1578        if !matches!(self.peek(), Token::RParen) {
1579            return Err(self.err(alloc::format!(
1580                "expected ')' to close the column list, got {:?}",
1581                self.peek()
1582            )));
1583        }
1584        self.advance(); // )
1585        Ok(cols)
1586    }
1587
1588    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1589    /// PUBLIC.
1590    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1591        if grant {
1592            if matches!(self.peek(), Token::To) {
1593                self.advance();
1594            } else {
1595                self.expect_keyword_ident("to")?;
1596            }
1597        } else if matches!(self.peek(), Token::From) {
1598            self.advance();
1599        } else {
1600            self.expect_keyword_ident("from")?;
1601        }
1602        let mut grantees: Vec<String> = Vec::new();
1603        loop {
1604            // `GROUP name` is the legacy spelling of a plain role name.
1605            if self.peek_keyword_ident("group") {
1606                self.advance();
1607            }
1608            if self.peek_keyword_ident("public") {
1609                self.advance();
1610                grantees.push(String::new()); // PUBLIC
1611            } else {
1612                grantees.push(self.expect_ident_like()?);
1613            }
1614            if matches!(self.peek(), Token::Comma) {
1615                self.advance();
1616            } else {
1617                break;
1618            }
1619        }
1620        Ok(grantees)
1621    }
1622
1623    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1624    /// The body keeps its `$N` placeholders; substitution happens at
1625    /// EXECUTE. The declared types are recorded for
1626    /// `pg_prepared_statements.parameter_types` but are not enforced —
1627    /// PG infers when the list is omitted, and SPG resolves the values
1628    /// at substitution time either way.
1629    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1630        let start = self.pos;
1631        self.advance(); // PREPARE
1632        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1633        // different statement that happens to share the keyword. PG
1634        // ships with `max_prepared_transactions = 0` and reports it
1635        // this way; SPG has no prepared-transaction registry, so the
1636        // same wording is the accurate answer rather than a dodge.
1637        // Round 277 turned this from a silent no-op into a confusing
1638        // "expected AS in PREPARE" parse error.
1639        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1640            self.advance();
1641            let gid = match self.advance() {
1642                Token::String(g) => g,
1643                other => {
1644                    return Err(self.err(alloc::format!(
1645                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1646                    )));
1647                }
1648            };
1649            return Ok(Statement::PrepareTransaction(gid));
1650        }
1651        let name = self.expect_ident_like()?;
1652        let mut param_types = Vec::new();
1653        if matches!(self.peek(), Token::LParen) {
1654            self.advance();
1655            loop {
1656                let mut ty = self.expect_ident_like()?;
1657                // A parameterised type name (`numeric(10,2)`,
1658                // `varchar(20)`) keeps its argument list in the text.
1659                if matches!(self.peek(), Token::LParen) {
1660                    let mut depth = 0usize;
1661                    let mut buf = String::from("(");
1662                    loop {
1663                        match self.advance() {
1664                            Token::LParen => {
1665                                depth += 1;
1666                                if depth > 1 {
1667                                    buf.push('(');
1668                                }
1669                            }
1670                            Token::RParen => {
1671                                depth -= 1;
1672                                buf.push(')');
1673                                if depth == 0 {
1674                                    break;
1675                                }
1676                            }
1677                            Token::Comma => buf.push(','),
1678                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1679                            Token::Eof => break,
1680                            _ => {}
1681                        }
1682                    }
1683                    ty.push_str(&buf);
1684                }
1685                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1686                // position, same family as the parameter list above.
1687                let array_suffix = self.consume_array_suffix();
1688                ty.push_str(&array_suffix);
1689                param_types.push(ty);
1690                match self.peek() {
1691                    Token::Comma => {
1692                        self.advance();
1693                    }
1694                    Token::RParen => {
1695                        self.advance();
1696                        break;
1697                    }
1698                    other => {
1699                        return Err(self.err(alloc::format!(
1700                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1701                        )));
1702                    }
1703                }
1704            }
1705        }
1706        if !matches!(self.peek(), Token::As) {
1707            return Err(self.err(alloc::format!(
1708                "expected AS in PREPARE, got {:?}",
1709                self.peek()
1710            )));
1711        }
1712        self.advance();
1713        let body = self.parse_one_statement()?;
1714        // The Parser holds tokens, not the source text, so the
1715        // statement PG reports in `pg_prepared_statements.statement`
1716        // is rebuilt from the AST rather than sliced from the input.
1717        let _ = start;
1718        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1719        if !param_types.is_empty() {
1720            source.push_str(" (");
1721            source.push_str(&param_types.join(", "));
1722            source.push(')');
1723        }
1724        source.push_str(" AS ");
1725        source.push_str(&alloc::format!("{body}"));
1726        Ok(Statement::Prepare {
1727            name,
1728            param_types,
1729            body: alloc::boxed::Box::new(body),
1730            source,
1731        })
1732    }
1733
1734    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1735    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1736        self.advance(); // EXECUTE
1737        let name = self.expect_ident_like()?;
1738        let mut args = Vec::new();
1739        if matches!(self.peek(), Token::LParen) {
1740            self.advance();
1741            if matches!(self.peek(), Token::RParen) {
1742                self.advance();
1743            } else {
1744                loop {
1745                    args.push(self.parse_expr(0)?);
1746                    match self.advance() {
1747                        Token::Comma => {}
1748                        Token::RParen => break,
1749                        other => {
1750                            return Err(self.err(alloc::format!(
1751                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1752                            )));
1753                        }
1754                    }
1755                }
1756            }
1757        }
1758        Ok(Statement::Execute { name, args })
1759    }
1760
1761    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1762    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1763    /// procedure catalog yet, so this reports PG's not-found error
1764    /// (with its HINT) rather than pretending the call ran.
1765    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1766    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1767    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1768        self.advance(); // DISCARD
1769        let target = match self.advance() {
1770            Token::All => DiscardTarget::All,
1771            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1772                "all" => DiscardTarget::All,
1773                "plans" => DiscardTarget::Plans,
1774                "sequences" => DiscardTarget::Sequences,
1775                "temp" | "temporary" => DiscardTarget::Temp,
1776                other => {
1777                    return Err(self.err(format!(
1778                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1779                    )));
1780                }
1781            },
1782            other => {
1783                return Err(self.err(format!(
1784                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785                )));
1786            }
1787        };
1788        Ok(Statement::Discard(target))
1789    }
1790
1791    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1792    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1793    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1794    /// aggressively the server interrupts, which SPG does not distinguish.
1795    /// Bare `KILL <id>` means CONNECTION.
1796    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1797        self.advance(); // KILL
1798        let mut query_only = false;
1799        loop {
1800            // CONNECTION is a reserved keyword token (it also opens
1801            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1802            // `Token::Connection` rather than a bare ident.
1803            if matches!(self.peek(), Token::Connection) {
1804                self.advance();
1805                break;
1806            }
1807            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1808                break;
1809            };
1810            match w.to_ascii_lowercase().as_str() {
1811                "hard" | "soft" => {
1812                    self.advance();
1813                }
1814                "query" => {
1815                    self.advance();
1816                    query_only = true;
1817                    break;
1818                }
1819                _ => break,
1820            }
1821        }
1822        let id = self.parse_expr(0)?;
1823        Ok(Statement::Kill {
1824            query_only,
1825            id: Box::new(id),
1826        })
1827    }
1828
1829    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1830        self.advance(); // CALL
1831        let name = self.expect_ident_like()?;
1832        self.consume_until_statement_boundary();
1833        Ok(Statement::Call(name))
1834    }
1835
1836    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1837        self.advance(); // DEALLOCATE
1838        // PG accepts an optional noise `PREPARE` keyword here.
1839        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1840            self.advance();
1841        }
1842        if matches!(self.peek(), Token::All) {
1843            self.advance();
1844            return Ok(Statement::Deallocate(None));
1845        }
1846        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1847            self.advance();
1848            return Ok(Statement::Deallocate(None));
1849        }
1850        let name = self.expect_ident_like()?;
1851        Ok(Statement::Deallocate(Some(name)))
1852    }
1853
1854    fn consume_until_statement_boundary(&mut self) {
1855        loop {
1856            match self.peek() {
1857                Token::Semicolon | Token::Eof => return,
1858                _ => self.advance(),
1859            };
1860        }
1861    }
1862
1863    /// v7.22 (round-13 T2) — consume to the statement boundary like
1864    /// `consume_until_statement_boundary`, but pick out the sequence
1865    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1866    /// columns) or the first string literal (`nextval('<seq>')`).
1867    /// Schema qualifiers and `::regclass` casts are stripped.
1868    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1869        let mut seq: Option<String> = None;
1870        let mut after_sequence_kw = false;
1871        let mut after_name_kw = false;
1872        loop {
1873            match self.peek().clone() {
1874                Token::Semicolon | Token::Eof => break,
1875                Token::Ident(s) | Token::QuotedIdent(s) => {
1876                    if after_name_kw && seq.is_none() {
1877                        self.advance();
1878                        let mut name = s;
1879                        // `SEQUENCE NAME public.groups_id_seq` — keep
1880                        // the bare name, drop qualifiers.
1881                        while matches!(self.peek(), Token::Dot) {
1882                            self.advance();
1883                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1884                                name = n;
1885                            }
1886                        }
1887                        seq = Some(name);
1888                        after_name_kw = false;
1889                        continue;
1890                    }
1891                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1892                        after_name_kw = true;
1893                        after_sequence_kw = false;
1894                    } else {
1895                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1896                    }
1897                    self.advance();
1898                }
1899                Token::String(s) => {
1900                    if seq.is_none() {
1901                        // `nextval('public.groups_id_seq'::regclass)`
1902                        let bare = s
1903                            .rsplit_once('.')
1904                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1905                        seq = Some(bare);
1906                    }
1907                    self.advance();
1908                }
1909                _ => {
1910                    after_sequence_kw = false;
1911                    after_name_kw = false;
1912                    self.advance();
1913                }
1914            }
1915        }
1916        seq
1917    }
1918
1919    /// v7.39 (round 621) — is the next token the keyword `BY`?
1920    ///
1921    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1922    /// column, table and alias name — and SPG lexed it into a dedicated
1923    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1924    /// two-letter keywords the lexer knew, this was the only one PG leaves
1925    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1926    ///
1927    /// The token is gone; the three clauses that own the word — GROUP BY,
1928    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1929    /// ask this instead. Adding it to the unreserved-identifier table was not
1930    /// enough on its own: identifier positions that match the token shape
1931    /// directly (an index's column list, a table alias) never consult that
1932    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1933    /// Not lexing it as a keyword closes the whole class rather than the two
1934    /// positions that happened to be noticed.
1935    fn peek_is_by(&self) -> bool {
1936        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1937    }
1938
1939    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1940    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1941    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1942    fn consume_drop_behaviour(&mut self) {
1943        if matches!(
1944            self.peek(),
1945            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1946        ) {
1947            self.advance();
1948        }
1949    }
1950
1951    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1952        let first = match self.advance() {
1953            Token::Ident(s) | Token::QuotedIdent(s) => s,
1954            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1955            // per PG's `pg_get_keywords()` classification. SPG tokenizes
1956            // these as named variants for parsing leverage in the
1957            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1958            // `BEGIN`, etc.), but they MUST still be usable as table /
1959            // column / alias names in DDL+DML. Sentori migrations like
1960            // 0001_init.sql ship `release TEXT NOT NULL` in the events
1961            // table — the `events.release` column carries the release
1962            // identifier string. Pre-T4 this triggered "expected
1963            // identifier, got Release" and blocked every drop-in user
1964            // whose schema had a column / alias with one of these names.
1965            other if unreserved_keyword_text(&other).is_some() => {
1966                unreserved_keyword_text(&other).unwrap()
1967            }
1968            other => {
1969                return Err(ParseError {
1970                    message: format!("expected identifier, got {other:?}"),
1971                    token_pos: self.consumed_pos(),
1972                });
1973            }
1974        };
1975        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1976        // qualify every name with `public.` (and pg_catalog.* for
1977        // functions); SPG is single-schema so we discard the
1978        // prefix and return only the trailing ident. Same shape
1979        // also handles MySQL `db.tbl` cross-database refs (SPG
1980        // ignores the db part).
1981        if matches!(self.peek(), Token::Dot) {
1982            self.advance();
1983            match self.advance() {
1984                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1985                other if unreserved_keyword_text(&other).is_some() => {
1986                    return Ok(unreserved_keyword_text(&other).unwrap());
1987                }
1988                other => {
1989                    return Err(ParseError {
1990                        message: format!("expected identifier after '{first}.', got {other:?}"),
1991                        token_pos: self.consumed_pos(),
1992                    });
1993                }
1994            }
1995        }
1996        Ok(first)
1997    }
1998
1999    #[allow(clippy::too_many_lines)]
2000    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2001        // v7.14.0 — empty / comment-only / semicolon-only input
2002        // (after the lexer strips line + block + MySQL
2003        // conditional comments) lands as Statement::Empty.
2004        // pg_dump and mysqldump emit several wrappers that
2005        // collapse to nothing after stripping (`/*!40101 SET …
2006        // */;`, blank lines between statements); the engine
2007        // returns CommandOk no-op so the dump loads cleanly.
2008        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2009            return Ok(Statement::Empty);
2010        }
2011        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2012        // catalog / metadata DDL that has no behavioural effect
2013        // on SPG's single-schema, single-database, single-user
2014        // model. Consume the whole statement up to the next
2015        // semicolon / EOF and return Empty. This is broader than
2016        // the per-keyword DROP / SET / COMMENT arms but lets the
2017        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2018        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2019        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2020        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2021            let lc = s.to_ascii_lowercase();
2022            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2023            if lc == "comment" {
2024                return self.parse_comment_on();
2025            }
2026            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2027            if lc == "grant" || lc == "revoke" {
2028                return self.parse_grant_or_revoke(lc == "grant");
2029            }
2030            // v7.39 (round 277) — the SQL-level prepared-statement
2031            // surface is REAL now. It used to be accepted and dropped
2032            // on the theory that "real execution still happens via the
2033            // extended-query flow" — true only for a driver that uses
2034            // that flow; a plain SQL PREPARE / EXECUTE returned no
2035            // rows at all.
2036            if lc == "prepare" {
2037                return self.parse_prepare();
2038            }
2039            if lc == "execute" {
2040                return self.parse_execute();
2041            }
2042            if lc == "deallocate" {
2043                return self.parse_deallocate();
2044            }
2045            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2046            // accepted and dropped, so an application's stored-procedure
2047            // invocation reported success and did nothing. SPG has no
2048            // procedure catalog, so every CALL names a procedure that
2049            // does not exist — which is exactly what PG says.
2050            if lc == "call" {
2051                return self.parse_call();
2052            }
2053            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2054            // names one connection and acts on it.
2055            if lc == "kill" {
2056                return self.parse_kill();
2057            }
2058            if lc == "discard" {
2059                return self.parse_discard();
2060            }
2061            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2062            // Still performs nothing; the roles are carried out so a name
2063            // that does not exist is refused, as PG18 refuses it.
2064            if lc == "reassign" {
2065                self.advance();
2066                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2067                    self.advance();
2068                }
2069                if self.peek_is_by() {
2070                    self.advance();
2071                }
2072                // Only the roles BEFORE the TO are the ones that must
2073                // exist — `TO` names the new owner, which PG checks as
2074                // well, so both lists are collected.
2075                let mut names = self.take_comma_separated_names();
2076                if matches!(self.peek(), Token::To) {
2077                    self.advance();
2078                    names.extend(self.take_comma_separated_names());
2079                }
2080                self.consume_until_statement_boundary();
2081                return Ok(Statement::ValidateOnly {
2082                    kind: crate::ast::ValidateOnlyKind::RoleName,
2083                    names,
2084                });
2085            }
2086            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2087            // unconditionally with `no security label providers have been
2088            // loaded`, whatever object it names, because none is loaded.
2089            // SPG has none either; accepting it told the caller a label had
2090            // been applied when nothing anywhere records one.
2091            if lc == "security" {
2092                self.consume_until_statement_boundary();
2093                return Ok(Statement::ValidateOnly {
2094                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2095                    names: Vec::new(),
2096                });
2097            }
2098            if is_dump_noise_statement(&lc) {
2099                self.consume_until_statement_boundary();
2100                return Ok(Statement::Empty);
2101            }
2102        }
2103        match self.peek() {
2104            Token::Select => self.parse_select_stmt(),
2105            // v7.37.17 (17.6 siblings) — a statement opening with a
2106            // parenthesized query group: `(SELECT … UNION …)
2107            // INTERSECT …`. parse_bare_select's group arm consumes
2108            // the parens; the select parser handles the outer chain
2109            // and tail.
2110            Token::LParen
2111                if matches!(
2112                    self.tokens.get(self.pos + 1),
2113                    Some(Token::Select | Token::LParen | Token::Values)
2114                ) =>
2115            {
2116                self.parse_select_stmt()
2117            }
2118            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2119            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2120            // Lowers to the same UNION ALL chain the FROM-position
2121            // form uses, then reuses the shared SELECT tail.
2122            Token::Values => {
2123                self.advance(); // VALUES
2124                let mut head = self.parse_values_rows_body()?;
2125                self.parse_select_tail_into(&mut head)?;
2126                Ok(Statement::Select(head))
2127            }
2128            // SQL-standard `TABLE name` shorthand for
2129            // `SELECT * FROM name` — pg_dump never emits it, but
2130            // psql users and PG docs use it constantly. Set-op
2131            // chains and the ORDER BY/LIMIT tail compose like any
2132            // SELECT head.
2133            Token::Table
2134                if matches!(
2135                    self.tokens.get(self.pos + 1),
2136                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2137                ) =>
2138            {
2139                let mut head = self.parse_table_shorthand()?;
2140                self.parse_setop_chain_into(&mut head)?;
2141                self.parse_select_tail_into(&mut head)?;
2142                Ok(Statement::Select(head))
2143            }
2144            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2145            // body is a dollar-quoted plpgsql block (lexer already
2146            // collapsed `$$…$$` into a single Token::String).
2147            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2148            // real PlPgSqlBlock so the engine can EXECUTE it at
2149            // top level instead of silently swallowing. Pre-
2150            // v7.16.2 the parser threw the body away and the
2151            // engine returned CommandOk for the entire DO; that
2152            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2153            // $$` into a SEV-1 silent no-op (the IF + the rename
2154            // were both invisible — mailrs's migrate-042 didn't
2155            // actually run). Now the body parses + executes;
2156            // EmbeddedSql inside the block runs immediately
2157            // against the engine (not deferred — we're at top
2158            // level, not inside a trigger row-write loop).
2159            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2160                self.advance();
2161                let body_text = match self.advance() {
2162                    Token::String(s) => s,
2163                    other => {
2164                        return Err(self.err(alloc::format!(
2165                            "expected dollar-quoted body after DO, got {other:?}"
2166                        )));
2167                    }
2168                };
2169                // Optional `LANGUAGE <name>` trailer (idents only).
2170                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2171                    self.advance();
2172                    let _ = self.expect_ident_like()?;
2173                }
2174                // Parse the body — same shape CREATE FUNCTION
2175                // uses for trigger function bodies. If the body
2176                // doesn't parse cleanly we surface the error
2177                // (better than silent no-op).
2178                let block = parse_plpgsql_body(&body_text)?;
2179                Ok(Statement::DoBlock(block))
2180            }
2181            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2182            // WITH isn't a reserved token in our lexer — comes through
2183            // as `Token::Ident("with")` (case-insensitive).
2184            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2185                self.advance();
2186                self.parse_with_cte_then_select()
2187            }
2188            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2189            // an identifier — not a reserved keyword.
2190            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2191                self.advance();
2192                let mut analyze = false;
2193                let mut suggest = false;
2194                let mut costs_off = false;
2195                let mut buffers = false;
2196                let mut timing_off = false;
2197                let mut settings = false;
2198                let mut wal = false;
2199                let mut summary_off = false;
2200                let mut format = crate::ast::ExplainFormat::Text;
2201                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2202                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2203                // options are comma-separated. Booleans default to ON
2204                // when the value token is omitted (matches PG).
2205                if matches!(self.peek(), Token::LParen) {
2206                    self.advance();
2207                    loop {
2208                        let opt = match self.peek().clone() {
2209                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2210                            other => {
2211                                return Err(self.err(format!(
2212                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2213                                )));
2214                            }
2215                        };
2216                        self.advance();
2217                        if opt.eq_ignore_ascii_case("suggest") {
2218                            suggest = true;
2219                            // SUGGEST takes no explicit value today.
2220                        } else if opt.eq_ignore_ascii_case("costs") {
2221                            // PG syntax: `COSTS [ON | OFF]`. Default
2222                            // when value omitted is ON, so plain
2223                            // `COSTS` is a no-op. `COSTS OFF` flips.
2224                            // `ON` lexes to `Token::On` (reserved
2225                            // keyword in JOIN ... ON contexts); accept
2226                            // it alongside the bare Ident form so the
2227                            // grammar matches PG verbatim.
2228                            let value = match self.peek().clone() {
2229                                Token::On => {
2230                                    self.advance();
2231                                    true
2232                                }
2233                                Token::Ident(v) | Token::QuotedIdent(v)
2234                                    if v.eq_ignore_ascii_case("off") =>
2235                                {
2236                                    self.advance();
2237                                    false
2238                                }
2239                                Token::Ident(v) | Token::QuotedIdent(v)
2240                                    if v.eq_ignore_ascii_case("true") =>
2241                                {
2242                                    self.advance();
2243                                    true
2244                                }
2245                                _ => true,
2246                            };
2247                            costs_off = !value;
2248                        } else if opt.eq_ignore_ascii_case("analyze")
2249                            || opt.eq_ignore_ascii_case("analyse")
2250                        {
2251                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2252                            // Same default-ON rule as ANALYZE keyword form.
2253                            let value = match self.peek().clone() {
2254                                Token::On => {
2255                                    self.advance();
2256                                    true
2257                                }
2258                                Token::Ident(v) | Token::QuotedIdent(v)
2259                                    if v.eq_ignore_ascii_case("off") =>
2260                                {
2261                                    self.advance();
2262                                    false
2263                                }
2264                                Token::Ident(v) | Token::QuotedIdent(v)
2265                                    if v.eq_ignore_ascii_case("true") =>
2266                                {
2267                                    self.advance();
2268                                    true
2269                                }
2270                                _ => true,
2271                            };
2272                            analyze = value;
2273                        } else if opt.eq_ignore_ascii_case("buffers") {
2274                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2275                            let value = match self.peek().clone() {
2276                                Token::On => {
2277                                    self.advance();
2278                                    true
2279                                }
2280                                Token::Ident(v) | Token::QuotedIdent(v)
2281                                    if v.eq_ignore_ascii_case("off") =>
2282                                {
2283                                    self.advance();
2284                                    false
2285                                }
2286                                Token::Ident(v) | Token::QuotedIdent(v)
2287                                    if v.eq_ignore_ascii_case("true") =>
2288                                {
2289                                    self.advance();
2290                                    true
2291                                }
2292                                _ => true,
2293                            };
2294                            buffers = value;
2295                        } else if opt.eq_ignore_ascii_case("timing") {
2296                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2297                            // the measured wall-clock annotation.
2298                            let value = match self.peek().clone() {
2299                                Token::On => {
2300                                    self.advance();
2301                                    true
2302                                }
2303                                Token::Ident(v) | Token::QuotedIdent(v)
2304                                    if v.eq_ignore_ascii_case("off") =>
2305                                {
2306                                    self.advance();
2307                                    false
2308                                }
2309                                Token::Ident(v) | Token::QuotedIdent(v)
2310                                    if v.eq_ignore_ascii_case("true") =>
2311                                {
2312                                    self.advance();
2313                                    true
2314                                }
2315                                _ => true,
2316                            };
2317                            timing_off = !value;
2318                        } else if opt.eq_ignore_ascii_case("settings") {
2319                            settings = true;
2320                        } else if opt.eq_ignore_ascii_case("wal") {
2321                            wal = true;
2322                        } else if opt.eq_ignore_ascii_case("summary") {
2323                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2324                            // gates the trailing Planning/Execution Time
2325                            // lines now (was accept-and-no-op).
2326                            let value = match self.peek().clone() {
2327                                Token::On => {
2328                                    self.advance();
2329                                    true
2330                                }
2331                                Token::Ident(v) | Token::QuotedIdent(v)
2332                                    if v.eq_ignore_ascii_case("off") =>
2333                                {
2334                                    self.advance();
2335                                    false
2336                                }
2337                                Token::Ident(v) | Token::QuotedIdent(v)
2338                                    if v.eq_ignore_ascii_case("true") =>
2339                                {
2340                                    self.advance();
2341                                    true
2342                                }
2343                                _ => true,
2344                            };
2345                            summary_off = !value;
2346                        } else if opt.eq_ignore_ascii_case("verbose")
2347                            || opt.eq_ignore_ascii_case("format")
2348                        {
2349                            // v7.37.22 — accept-but-no-op the remaining
2350                            // PG options so EXPLAIN-using clients
2351                            // (pgAdmin / DataGrip) don't see syntax
2352                            // errors. FORMAT takes a value (text /
2353                            // json / yaml / xml); skip the next token
2354                            // if it's an ident.
2355                            if opt.eq_ignore_ascii_case("format") {
2356                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2357                                {
2358                                    self.advance();
2359                                    format = match v.to_ascii_lowercase().as_str() {
2360                                        "text" => crate::ast::ExplainFormat::Text,
2361                                        "json" => crate::ast::ExplainFormat::Json,
2362                                        "xml" => crate::ast::ExplainFormat::Xml,
2363                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2364                                        other => {
2365                                            return Err(self.err(format!(
2366                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2367                                                 supports text, json, xml, yaml"
2368                                            )));
2369                                        }
2370                                    };
2371                                }
2372                            } else {
2373                                // VERBOSE / SUMMARY take optional ON/OFF;
2374                                // consume if present.
2375                                if matches!(self.peek(), Token::On) {
2376                                    self.advance();
2377                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2378                                    self.peek().clone()
2379                                    && (v.eq_ignore_ascii_case("off")
2380                                        || v.eq_ignore_ascii_case("true"))
2381                                {
2382                                    self.advance();
2383                                    let _ = v;
2384                                }
2385                            }
2386                        } else {
2387                            return Err(self.err(format!(
2388                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2389                            )));
2390                        }
2391                        if matches!(self.peek(), Token::Comma) {
2392                            self.advance();
2393                            continue;
2394                        }
2395                        break;
2396                    }
2397                    if !matches!(self.peek(), Token::RParen) {
2398                        return Err(self.err(format!(
2399                            "expected ')' after EXPLAIN options, got {:?}",
2400                            self.peek()
2401                        )));
2402                    }
2403                    self.advance();
2404                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2405                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2406                {
2407                    self.advance();
2408                    analyze = true;
2409                }
2410                // v7.39 (round 224) — the body may open with WITH (CTEs);
2411                // route through the same CTE-then-SELECT path the top-level
2412                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2413                // too (PG explains INSERT / UPDATE / DELETE).
2414                let inner = match self.peek().clone() {
2415                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2416                        self.advance();
2417                        self.parse_with_cte_then_select()?
2418                    }
2419                    Token::Insert => self.parse_insert_stmt(false)?,
2420                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2421                        self.advance();
2422                        self.parse_update_after_keyword()?
2423                    }
2424                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2425                        self.advance();
2426                        self.parse_delete_after_keyword()?
2427                    }
2428                    _ => self.parse_select_stmt()?,
2429                };
2430                if !matches!(
2431                    inner,
2432                    Statement::Select(_)
2433                        | Statement::Insert(_)
2434                        | Statement::Update(_)
2435                        | Statement::Delete(_)
2436                ) {
2437                    return Err(self.err(format!(
2438                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2439                    )));
2440                }
2441                Ok(Statement::Explain(crate::ast::ExplainStatement {
2442                    analyze,
2443                    inner: Box::new(inner),
2444                    suggest,
2445                    costs_off,
2446                    buffers,
2447                    timing_off,
2448                    settings,
2449                    wal,
2450                    summary_off,
2451                    format,
2452                }))
2453            }
2454            Token::Create => self.parse_create_stmt(),
2455            Token::Insert => self.parse_insert_stmt(false),
2456            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2457            // spelling; route to the same handler. DESC is the
2458            // reserved ORDER BY token, so it gets its own arm.
2459            Token::Ident(s)
2460                if s.eq_ignore_ascii_case("describe")
2461                    && matches!(
2462                        self.tokens.get(self.pos + 1),
2463                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2464                    ) =>
2465            {
2466                self.advance();
2467                let table = self.expect_ident_like()?;
2468                Ok(Statement::ShowColumns(table))
2469            }
2470            Token::Desc
2471                if matches!(
2472                    self.tokens.get(self.pos + 1),
2473                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2474                ) =>
2475            {
2476                self.advance();
2477                let table = self.expect_ident_like()?;
2478                Ok(Statement::ShowColumns(table))
2479            }
2480            // `COPY table [(cols)] TO STDOUT` — the export half of
2481            // pg_dump's COPY pair (the FROM stdin half rides the
2482            // embed import path). Options need a format design and
2483            // error honestly.
2484            Token::Ident(s)
2485                if s.eq_ignore_ascii_case("copy")
2486                    && matches!(
2487                        self.tokens.get(self.pos + 1),
2488                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2489                    ) =>
2490            {
2491                self.advance(); // COPY
2492                let table = self.expect_ident_like()?;
2493                let columns = if matches!(self.peek(), Token::LParen) {
2494                    self.advance();
2495                    let mut cols = alloc::vec![self.expect_ident_like()?];
2496                    while matches!(self.peek(), Token::Comma) {
2497                        self.advance();
2498                        cols.push(self.expect_ident_like()?);
2499                    }
2500                    if !matches!(self.peek(), Token::RParen) {
2501                        return Err(self.err(format!(
2502                            "expected ')' after COPY column list, got {:?}",
2503                            self.peek()
2504                        )));
2505                    }
2506                    self.advance();
2507                    Some(cols)
2508                } else {
2509                    None
2510                };
2511                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2512                // endpoint. (FROM STDIN still rides the wire/import path —
2513                // its data arrives out of band.)
2514                if matches!(self.peek(), Token::From)
2515                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2516                {
2517                    self.advance(); // FROM
2518                    let Token::String(path) = self.advance() else {
2519                        unreachable!()
2520                    };
2521                    let options = self.parse_copy_to_options()?;
2522                    return Ok(Statement::CopyFromFile {
2523                        table,
2524                        columns,
2525                        path,
2526                        options,
2527                    });
2528                }
2529                if !matches!(self.peek(), Token::To) {
2530                    return Err(self.err(format!(
2531                        "COPY: only TO STDOUT is supported here (FROM stdin \
2532                         rides the import path); got {:?}",
2533                        self.peek()
2534                    )));
2535                }
2536                self.advance();
2537                if matches!(self.peek(), Token::String(_)) {
2538                    let Token::String(path) = self.advance() else { unreachable!() };
2539                    let options = self.parse_copy_to_options()?;
2540                    return Ok(Statement::CopyToFile {
2541                        table,
2542                        columns,
2543                        query: None,
2544                        path,
2545                        options,
2546                    });
2547                }
2548                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2549                    return Err(self.err(format!(
2550                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2551                        self.peek()
2552                    )));
2553                }
2554                self.advance();
2555                let options = self.parse_copy_to_options()?;
2556                Ok(Statement::CopyTo {
2557                    table,
2558                    columns,
2559                    query: None,
2560                    options,
2561                })
2562            }
2563            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2564            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2565            // result set is streamed in COPY format (PG's query form).
2566            Token::Ident(s)
2567                if s.eq_ignore_ascii_case("copy")
2568                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2569            {
2570                self.advance(); // COPY
2571                self.advance(); // (
2572                let query = self.parse_select_stmt()?;
2573                if !matches!(self.peek(), Token::RParen) {
2574                    return Err(self.err(format!(
2575                        "expected ')' after COPY query, got {:?}",
2576                        self.peek()
2577                    )));
2578                }
2579                self.advance(); // )
2580                if !matches!(self.peek(), Token::To) {
2581                    return Err(self.err(format!(
2582                        "COPY (query): only TO STDOUT is supported, got {:?}",
2583                        self.peek()
2584                    )));
2585                }
2586                self.advance();
2587                if matches!(self.peek(), Token::String(_)) {
2588                    let Token::String(path) = self.advance() else { unreachable!() };
2589                    let options = self.parse_copy_to_options()?;
2590                    return Ok(Statement::CopyToFile {
2591                        table: String::new(),
2592                        columns: None,
2593                        query: Some(alloc::boxed::Box::new(query)),
2594                        path,
2595                        options,
2596                    });
2597                }
2598                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2599                    return Err(self.err(format!(
2600                        "COPY (query): TO supports STDOUT only, got {:?}",
2601                        self.peek()
2602                    )));
2603                }
2604                self.advance();
2605                let options = self.parse_copy_to_options()?;
2606                Ok(Statement::CopyTo {
2607                    table: String::new(),
2608                    columns: None,
2609                    query: Some(alloc::boxed::Box::new(query)),
2610                    options,
2611                })
2612            }
2613            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2614            // Shares the INSERT body; the replace flag lowers it
2615            // onto ON CONFLICT DO UPDATE with an empty assignment
2616            // list (engine: replace the whole row).
2617            Token::Ident(s)
2618                if s.eq_ignore_ascii_case("replace")
2619                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2620            {
2621                self.parse_insert_stmt(true)
2622            }
2623            Token::Begin => {
2624                self.advance();
2625                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2626                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2627                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2628                // is consumed first, then the trailing modes — including the
2629                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2630                // WORK/TRANSACTION). The explicit level, when present, rides the
2631                // statement so `exec_begin` applies it for this transaction.
2632                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2633                {
2634                    self.advance();
2635                }
2636                let iso = self.parse_isolation_level_clauses()?;
2637                Ok(Statement::Begin(iso))
2638            }
2639            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2640            // for BEGIN. START is contextual in PG too; pattern-match
2641            // on the ident here. Iso clauses are parse-and-ignored,
2642            // same as BEGIN above.
2643            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2644                self.advance();
2645                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2646                {
2647                    return Err(self.err(alloc::format!(
2648                        "expected TRANSACTION after START, got {:?}",
2649                        self.peek()
2650                    )));
2651                }
2652                self.advance();
2653                let iso = self.parse_isolation_level_clauses()?;
2654                Ok(Statement::Begin(iso))
2655            }
2656            Token::Commit => {
2657                self.advance();
2658                Ok(Statement::Commit)
2659            }
2660            Token::Rollback => {
2661                self.advance();
2662                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2663                // savepoint without ending the transaction. Bare
2664                // `ROLLBACK` drops the whole TX.
2665                if matches!(self.peek(), Token::To) {
2666                    self.advance();
2667                    if matches!(self.peek(), Token::Savepoint) {
2668                        self.advance();
2669                    }
2670                    let name = self.expect_ident_like()?;
2671                    Ok(Statement::RollbackToSavepoint(name))
2672                } else {
2673                    Ok(Statement::Rollback)
2674                }
2675            }
2676            Token::Savepoint => {
2677                self.advance();
2678                let name = self.expect_ident_like()?;
2679                Ok(Statement::Savepoint(name))
2680            }
2681            Token::Release => {
2682                self.advance();
2683                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2684                // is optional in standard SQL.
2685                if matches!(self.peek(), Token::Savepoint) {
2686                    self.advance();
2687                }
2688                let name = self.expect_ident_like()?;
2689                Ok(Statement::ReleaseSavepoint(name))
2690            }
2691            Token::Show => {
2692                self.advance();
2693                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2694                // v6.1.2 promoted TABLES to a reserved keyword (for
2695                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2696                // arrives as `Token::Tables` rather than a bare ident.
2697                // USERS / COLUMNS remain bare idents.
2698                let target = match self.advance() {
2699                    Token::Tables => "tables".to_string(),
2700                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2701                    // keyword token; recognise it as the SHOW CREATE
2702                    // dispatch keyword too.
2703                    Token::Create => "create".to_string(),
2704                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2705                    // keyword too; let SHOW INDEX FROM parse.
2706                    Token::Index => "index".to_string(),
2707                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2708                    // reserved (used in aggregate function calls);
2709                    // recognise it here so the parser dispatches
2710                    // to ShowParameter("all") — the engine returns
2711                    // the curated parameter inventory.
2712                    Token::All => "all".to_string(),
2713                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2714                    other => {
2715                        return Err(self.err(format!(
2716                            "expected SHOW target, got {other:?}"
2717                        )));
2718                    }
2719                };
2720                match target.as_str() {
2721                    "tables" => Ok(Statement::ShowTables),
2722                    "users" => Ok(Statement::ShowUsers),
2723                    // v7.38 轴 4 — `SHOW transaction_isolation`
2724                    // returns the currently-selected isolation level.
2725                    "transaction_isolation" => Ok(Statement::ShowParameter(
2726                        "transaction_isolation".to_string(),
2727                    )),
2728                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2729                    // TABLE <t>` returns a 2-column row: (Table,
2730                    // Create Table). mysqldump emits this for every
2731                    // table at scrape time; without it the dump
2732                    // round-trip stalls.
2733                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2734                    // FROM <t>` (also spelled `SHOW INDEX` and
2735                    // `SHOW KEYS`). admin / mysqldump probes use
2736                    // it to list per-table indexes.
2737                    "indexes" | "index" | "keys" => {
2738                        if !matches!(self.peek(), Token::From) {
2739                            return Err(self.err(format!(
2740                                "expected FROM after SHOW INDEXES, got {:?}",
2741                                self.peek()
2742                            )));
2743                        }
2744                        self.advance();
2745                        let table = self.expect_ident_like()?;
2746                        Ok(Statement::ShowIndexes(table))
2747                    }
2748                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2749                    // `SHOW VARIABLES`. Both return a 2-column row
2750                    // set listing server-side state; clients probe
2751                    // them at connect time.
2752                    "status" => Ok(Statement::ShowStatus),
2753                    "variables" => Ok(Statement::ShowVariables),
2754                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2755                    "processlist" => Ok(Statement::ShowProcesslist),
2756                    "create" => {
2757                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2758                        // TABLE is supported in v7.17.
2759                        let kind = match self.advance() {
2760                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2761                            Token::Table => "table".to_string(),
2762                            other => {
2763                                return Err(self.err(format!(
2764                                    "expected TABLE after SHOW CREATE, got {other:?}"
2765                                )));
2766                            }
2767                        };
2768                        if !kind.eq_ignore_ascii_case("table") {
2769                            return Err(self.err(format!(
2770                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2771                            )));
2772                        }
2773                        let name = self.expect_ident_like()?;
2774                        Ok(Statement::ShowCreateTable(name))
2775                    }
2776                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2777                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2778                    // it to populate the database selector at connect
2779                    // time; without it `mysql -p` errors before the
2780                    // first user query.
2781                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2782                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2783                    // keyword on its own; it lands here as a bare
2784                    // ident. Returning all publications + their
2785                    // scope summary.
2786                    "publications" => Ok(Statement::ShowPublications),
2787                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2788                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2789                    "columns" => {
2790                        if !matches!(self.peek(), Token::From) {
2791                            return Err(self.err(format!(
2792                                "expected FROM after SHOW COLUMNS, got {:?}",
2793                                self.peek()
2794                            )));
2795                        }
2796                        self.advance();
2797                        let table = self.expect_ident_like()?;
2798                        Ok(Statement::ShowColumns(table))
2799                    }
2800                    // v7.38 轴 4 surface — `SHOW <param>` for any
2801                    // remaining session / preset parameter name
2802                    // (server_version, search_path, client_encoding,
2803                    // …). The engine's ShowParameter handler does the
2804                    // dispatch; unrecognised names error there with
2805                    // a pointer to pg_settings, not at parse time —
2806                    // so a driver that issues `SHOW spam_setting`
2807                    // gets a clear runtime error instead of a
2808                    // confusing "unknown SHOW target".
2809                    other => {
2810                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2811                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2812                        // consume the dotted tail so it round-trips with
2813                        // `SET app.foo` / `current_setting('app.foo')`.
2814                        let mut full = other.to_string();
2815                        while matches!(self.peek(), Token::Dot) {
2816                            self.advance();
2817                            let seg = self.expect_ident_like()?;
2818                            full.push('.');
2819                            full.push_str(&seg.to_ascii_lowercase());
2820                        }
2821                        Ok(Statement::ShowParameter(full))
2822                    }
2823                }
2824            }
2825            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2826            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2827            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2828            // arrived as a bare ident; tokenising it dedicatedly
2829            // keeps the dispatch tree small.
2830            Token::Drop => {
2831                self.advance();
2832                match self.peek() {
2833                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2834                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2835                    // around DROP ROLE cleanup. SPG has no role-owner
2836                    // model, so consume to boundary as a no-op.
2837                    Token::Ident(s) | Token::QuotedIdent(s)
2838                        if s.eq_ignore_ascii_case("owned") =>
2839                    {
2840                        // v7.39 (round 696) — still a no-op (SPG has no
2841                        // role-owner model), but the ROLE is carried out so
2842                        // the engine can refuse one that does not exist,
2843                        // which is what PG18 does.
2844                        self.advance();
2845                        if self.peek_is_by() {
2846                            self.advance();
2847                        }
2848                        let names = self.take_comma_separated_names();
2849                        self.consume_until_statement_boundary();
2850                        Ok(Statement::ValidateOnly {
2851                            kind: crate::ast::ValidateOnlyKind::RoleName,
2852                            names,
2853                        })
2854                    }
2855                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2856                    // It drops only a TEMPORARY table, and name resolution
2857                    // already prefers the session's own, so the keyword is
2858                    // consumed and the ordinary DROP TABLE path runs.
2859                    Token::Ident(s) | Token::QuotedIdent(s)
2860                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2861                    {
2862                        self.advance();
2863                        if !matches!(self.peek(), Token::Table) {
2864                            return Err(self.err(alloc::format!(
2865                                "expected TABLE after DROP TEMPORARY, got {:?}",
2866                                self.peek()
2867                            )));
2868                        }
2869                        self.parse_drop_table_after_keyword()
2870                    }
2871                    Token::Publication => {
2872                        self.advance();
2873                        // v7.39 (round 754, F31-B4) — the round-753
2874                        // audit probe tripped over the missing
2875                        // `IF EXISTS` here (syntax error).
2876                        let if_exists = self.consume_if_exists();
2877                        let name = self.expect_ident_or_string()?;
2878                        Ok(Statement::DropPublication { name, if_exists })
2879                    }
2880                    Token::Subscription => {
2881                        self.advance();
2882                        let if_exists = self.consume_if_exists();
2883                        let name = self.expect_ident_or_string()?;
2884                        Ok(Statement::DropSubscription { name, if_exists })
2885                    }
2886                    Token::Ident(s) | Token::QuotedIdent(s)
2887                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2888                    {
2889                        self.advance();
2890                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2891                        // login user IS a role in PG, and SPG's store holds
2892                        // both. `IF EXISTS` is accepted on either spelling.
2893                        let if_exists = self.consume_if_exists();
2894                        let name = self.expect_ident_or_string()?;
2895                        Ok(Statement::DropUser { name, if_exists })
2896                    }
2897                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2898                    // CREATE DATABASE has parsed since v7.14 and this did
2899                    // not, so `DROP DATABASE IF EXISTS x` — what every
2900                    // teardown script and pg_dumpall preamble opens with —
2901                    // came back as a syntax error, which IF EXISTS cannot
2902                    // soften. The name is carried so the engine can answer
2903                    // the way PG does; PG never lets this succeed on a
2904                    // single-database server, since the name is either
2905                    // unknown ("database … does not exist", or a notice
2906                    // under IF EXISTS) or the one you are connected to
2907                    // ("cannot drop the currently open database").
2908                    Token::Ident(s) | Token::QuotedIdent(s)
2909                        if s.eq_ignore_ascii_case("database") =>
2910                    {
2911                        self.advance();
2912                        let if_exists = self.consume_if_exists();
2913                        let name = self.expect_ident_or_string()?;
2914                        self.consume_until_statement_boundary();
2915                        Ok(Statement::DropDatabase { name, if_exists })
2916                    }
2917                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2918                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2919                        self.advance();
2920                        let if_exists = self.consume_if_exists();
2921                        let name = self.expect_ident_like()?;
2922                        // ON <table>
2923                        if !matches!(self.peek(), Token::On) {
2924                            return Err(self.err(alloc::format!(
2925                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2926                                self.peek()
2927                            )));
2928                        }
2929                        self.advance();
2930                        let table = self.expect_ident_like()?;
2931                        Ok(Statement::DropTrigger {
2932                            name,
2933                            table,
2934                            if_exists,
2935                        })
2936                    }
2937                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2938                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2939                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2940                        self.advance();
2941                        let if_exists = self.consume_if_exists();
2942                        let name = self.expect_ident_like()?;
2943                        if !matches!(self.peek(), Token::On) {
2944                            return Err(self.err(alloc::format!(
2945                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
2946                                self.peek()
2947                            )));
2948                        }
2949                        self.advance();
2950                        let table = self.expect_ident_like()?;
2951                        // Optional CASCADE / RESTRICT — accepted, no effect.
2952                        self.consume_until_statement_boundary();
2953                        Ok(Statement::DropRule {
2954                            name,
2955                            table,
2956                            if_exists,
2957                        })
2958                    }
2959                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2960                    // v7.12.4 ignores any optional arg-list (signature-
2961                    // based overload disambiguation lands in v7.12.5+).
2962                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2963                        self.advance();
2964                        let if_exists = self.consume_if_exists();
2965                        let name = self.expect_ident_like()?;
2966                        // v7.39 (read01 round 62) — the argument list identifies
2967                        // WHICH overload to drop, so it is captured, not
2968                        // discarded. `DROP FUNCTION f` (no list) is legal when
2969                        // the name is unambiguous; the engine enforces that.
2970                        let args = if matches!(self.peek(), Token::LParen) {
2971                            Some(self.parse_function_signature_types()?)
2972                        } else {
2973                            None
2974                        };
2975                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2976                        // trailer, which `DROP TABLE` and `DROP INDEX` have
2977                        // accepted since v7.14 and this one refused outright.
2978                        // pg_dump writes it, so refusing was a parse error in
2979                        // the middle of a restore. SPG drops the function
2980                        // either way — it tracks no dependents to cascade to —
2981                        // which is the same reading the other two give it.
2982                        self.consume_drop_behaviour();
2983                        Ok(Statement::DropFunction {
2984                            name,
2985                            args,
2986                            if_exists,
2987                        })
2988                    }
2989                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2990                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2991                    // emit DROP TABLE IF EXISTS at the head of every
2992                    // CREATE TABLE block so re-importing a dump
2993                    // overwrites prior state. SPG accepts and removes
2994                    // matching tables; CASCADE/RESTRICT trailers
2995                    // accepted silently.
2996                    Token::Table => self.parse_drop_table_after_keyword(),
2997                    // v7.14.0 — DROP INDEX [IF EXISTS] name
2998                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
2999                    // for partial-index renames and pgvector
3000                    // migrations. SPG removes the matching index;
3001                    // IF EXISTS makes the drop idempotent.
3002                    Token::Index => {
3003                        self.advance();
3004                        let if_exists = self.consume_if_exists();
3005                        let name = self.expect_ident_like()?;
3006                        if matches!(
3007                            self.peek(),
3008                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3009                                || s.eq_ignore_ascii_case("restrict")
3010                        ) {
3011                            self.advance();
3012                        }
3013                        Ok(Statement::DropIndex { name, if_exists })
3014                    }
3015                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3016                    // [CASCADE|RESTRICT]. SPG is single-database;
3017                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3018                    // name [, name…] [CASCADE | RESTRICT]. Real
3019                    // unregister (was silent no-op pre-v7.17).
3020                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3021                        self.advance();
3022                        let if_exists = self.consume_if_exists();
3023                        let mut names = vec![self.expect_ident_like()?];
3024                        while matches!(self.peek(), Token::Comma) {
3025                            self.advance();
3026                            names.push(self.expect_ident_like()?);
3027                        }
3028                        if matches!(
3029                            self.peek(),
3030                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3031                                || s.eq_ignore_ascii_case("restrict")
3032                        ) {
3033                            self.advance();
3034                        }
3035                        Ok(Statement::DropSchema { names, if_exists })
3036                    }
3037                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3038                    // name [, name…] [CASCADE|RESTRICT].
3039                    Token::Ident(s) | Token::QuotedIdent(s)
3040                        if s.eq_ignore_ascii_case("type") =>
3041                    {
3042                        self.advance();
3043                        let if_exists = self.consume_if_exists();
3044                        let mut names = vec![self.expect_ident_like()?];
3045                        while matches!(self.peek(), Token::Comma) {
3046                            self.advance();
3047                            names.push(self.expect_ident_like()?);
3048                        }
3049                        if matches!(
3050                            self.peek(),
3051                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3052                                || s.eq_ignore_ascii_case("restrict")
3053                        ) {
3054                            self.advance();
3055                        }
3056                        Ok(Statement::DropType { names, if_exists })
3057                    }
3058                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3059                    // name [, name…] [CASCADE|RESTRICT].
3060                    Token::Ident(s) | Token::QuotedIdent(s)
3061                        if s.eq_ignore_ascii_case("domain") =>
3062                    {
3063                        self.advance();
3064                        let if_exists = self.consume_if_exists();
3065                        let mut names = vec![self.expect_ident_like()?];
3066                        while matches!(self.peek(), Token::Comma) {
3067                            self.advance();
3068                            names.push(self.expect_ident_like()?);
3069                        }
3070                        if matches!(
3071                            self.peek(),
3072                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3073                                || s.eq_ignore_ascii_case("restrict")
3074                        ) {
3075                            self.advance();
3076                        }
3077                        Ok(Statement::DropDomain { names, if_exists })
3078                    }
3079                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3080                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3081                    Token::Ident(s) | Token::QuotedIdent(s)
3082                        if s.eq_ignore_ascii_case("materialized") =>
3083                    {
3084                        self.advance();
3085                        let nxt = self.peek().clone();
3086                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3087                        {
3088                            return Err(self.err(alloc::format!(
3089                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3090                            )));
3091                        }
3092                        self.advance();
3093                        let if_exists = self.consume_if_exists();
3094                        let mut names = vec![self.expect_ident_like()?];
3095                        while matches!(self.peek(), Token::Comma) {
3096                            self.advance();
3097                            names.push(self.expect_ident_like()?);
3098                        }
3099                        if matches!(
3100                            self.peek(),
3101                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3102                                || s.eq_ignore_ascii_case("restrict")
3103                        ) {
3104                            self.advance();
3105                        }
3106                        Ok(Statement::DropMaterializedView { names, if_exists })
3107                    }
3108                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3109                    // name [, name…] [CASCADE|RESTRICT].
3110                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3111                        self.advance();
3112                        let if_exists = self.consume_if_exists();
3113                        let mut names = vec![self.expect_ident_like()?];
3114                        while matches!(self.peek(), Token::Comma) {
3115                            self.advance();
3116                            names.push(self.expect_ident_like()?);
3117                        }
3118                        if matches!(
3119                            self.peek(),
3120                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3121                                || s.eq_ignore_ascii_case("restrict")
3122                        ) {
3123                            self.advance();
3124                        }
3125                        Ok(Statement::DropView { names, if_exists })
3126                    }
3127                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3128                    // [CASCADE|RESTRICT]. Real removal from catalog
3129                    // (was a silent no-op pre-v7.17).
3130                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3131                        self.advance();
3132                        let if_exists = self.consume_if_exists();
3133                        let mut names = vec![self.expect_ident_like()?];
3134                        while matches!(self.peek(), Token::Comma) {
3135                            self.advance();
3136                            names.push(self.expect_ident_like()?);
3137                        }
3138                        if matches!(
3139                            self.peek(),
3140                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3141                                || s.eq_ignore_ascii_case("restrict")
3142                        ) {
3143                            self.advance();
3144                        }
3145                        Ok(Statement::DropSequence { names, if_exists })
3146                    }
3147                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3148                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3149                        self.advance();
3150                        self.parse_drop_policy_after_keyword()
3151                    }
3152                    // v7.37.17 (17.6 siblings) — DROP <target> for
3153                    // targets SPG doesn't natively track. pg_dump
3154                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3155                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3156                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3157                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3158                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3159                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3160                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3161                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3162                    // etc. — accept + Empty-return so pg_dump tails
3163                    // load through. Materialized-view drop dispatches
3164                    // to the existing DropTable path when the token
3165                    // is Materialized-View-shaped (elsewhere in
3166                    // this parser).
3167                    Token::Ident(s) | Token::QuotedIdent(s)
3168                        if s.eq_ignore_ascii_case("text")
3169                            // The DROP dispatch matches on PEEK — `text` is
3170                            // not yet consumed, so SEARCH/CONFIGURATION sit
3171                            // at pos+1/pos+2 (the round-695 trap's mirror).
3172                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3173                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3174                    {
3175                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3176                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3177                        // stay in the noise arm below.
3178                        self.advance(); // TEXT
3179                        self.advance(); // SEARCH
3180                        self.advance(); // CONFIGURATION
3181                        let if_exists = self.consume_if_exists();
3182                        let names = self.take_comma_separated_names();
3183                        self.consume_until_statement_boundary();
3184                        if if_exists {
3185                            return Ok(Statement::Empty);
3186                        }
3187                        Ok(Statement::ValidateOnly {
3188                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3189                            names,
3190                        })
3191                    }
3192                    Token::Ident(s) | Token::QuotedIdent(s)
3193                        if matches!(
3194                            s.to_ascii_lowercase().as_str(),
3195                            "type"
3196                                | "domain"
3197                                | "operator"
3198                                | "cast"
3199                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3200                                // TEMPLATE (CONFIGURATION intercepted above).
3201                                | "text"
3202                                | "materialized"
3203                                | "large"
3204                                | "role"
3205                                | "access"
3206                                | "procedure"
3207                                | "routine"
3208                        ) =>
3209                    {
3210                        self.consume_until_statement_boundary();
3211                        Ok(Statement::Empty)
3212                    }
3213                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3214                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3215                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3216                    // foreign-data warning family (round 706) so a
3217                    // CREATE→DROP sequence in a dump stays consistent.
3218                    Token::Ident(s) | Token::QuotedIdent(s)
3219                        if s.eq_ignore_ascii_case("server")
3220                            || s.eq_ignore_ascii_case("foreign") =>
3221                    {
3222                        self.advance();
3223                        self.consume_until_statement_boundary();
3224                        Ok(Statement::ValidateOnly {
3225                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3226                            names: Vec::new(),
3227                        })
3228                    }
3229                    Token::Ident(s) | Token::QuotedIdent(s)
3230                        if s.eq_ignore_ascii_case("collation")
3231                            || s.eq_ignore_ascii_case("tablespace") =>
3232                    {
3233                        let kind = if s.eq_ignore_ascii_case("collation") {
3234                            crate::ast::ValidateOnlyKind::CollationName
3235                        } else {
3236                            crate::ast::ValidateOnlyKind::TablespaceName
3237                        };
3238                        self.advance();
3239                        let if_exists = self.consume_if_exists();
3240                        let names = self.take_comma_separated_names();
3241                        self.consume_until_statement_boundary();
3242                        if if_exists {
3243                            return Ok(Statement::Empty);
3244                        }
3245                        Ok(Statement::ValidateOnly { kind, names })
3246                    }
3247                    Token::Ident(s) | Token::QuotedIdent(s)
3248                        if s.eq_ignore_ascii_case("event") =>
3249                    {
3250                        self.advance();
3251                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3252                        {
3253                            self.advance();
3254                        }
3255                        let if_exists = self.consume_if_exists();
3256                        let names = self.take_comma_separated_names();
3257                        self.consume_until_statement_boundary();
3258                        if if_exists {
3259                            return Ok(Statement::Empty);
3260                        }
3261                        Ok(Statement::ValidateOnly {
3262                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3263                            names,
3264                        })
3265                    }
3266                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3267                    // leave the noise list; see the ValidateOnly kinds.
3268                    Token::Ident(s) | Token::QuotedIdent(s)
3269                        if s.eq_ignore_ascii_case("conversion")
3270                            || s.eq_ignore_ascii_case("language")
3271                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3272                            // FIRST — the first draft looked for it after.
3273                            || s.eq_ignore_ascii_case("procedural") =>
3274                    {
3275                        let kind = if s.eq_ignore_ascii_case("conversion") {
3276                            crate::ast::ValidateOnlyKind::ConversionName
3277                        } else {
3278                            crate::ast::ValidateOnlyKind::LanguageName
3279                        };
3280                        self.advance();
3281                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3282                        {
3283                            self.advance();
3284                        }
3285                        let if_exists = self.consume_if_exists();
3286                        let names = self.take_comma_separated_names();
3287                        self.consume_until_statement_boundary();
3288                        if if_exists {
3289                            return Ok(Statement::Empty);
3290                        }
3291                        Ok(Statement::ValidateOnly { kind, names })
3292                    }
3293                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3294                    // name(argtypes)[, …]`. Parsed for real so the engine
3295                    // can answer as PG does; see Statement::DropAggregate.
3296                    Token::Ident(s) | Token::QuotedIdent(s)
3297                        if s.eq_ignore_ascii_case("aggregate") =>
3298                    {
3299                        self.advance();
3300                        let if_exists = self.consume_if_exists();
3301                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3302                        loop {
3303                            let name = self.expect_ident_like()?;
3304                            if !matches!(self.peek(), Token::LParen) {
3305                                return Err(self.err(alloc::format!(
3306                                    "expected argument list after DROP AGGREGATE {name}"
3307                                )));
3308                            }
3309                            self.advance();
3310                            let mut args: Vec<String> = Vec::new();
3311                            let mut star = false;
3312                            loop {
3313                                match self.peek().clone() {
3314                                    Token::RParen => {
3315                                        self.advance();
3316                                        break;
3317                                    }
3318                                    Token::Star => {
3319                                        self.advance();
3320                                        star = true;
3321                                    }
3322                                    Token::Comma => {
3323                                        self.advance();
3324                                    }
3325                                    _ => {
3326                                        // A type name may be multi-token
3327                                        // (`double precision`); glue idents
3328                                        // until , or ).
3329                                        let mut t = self.expect_ident_like()?;
3330                                        while let Token::Ident(nx) = self.peek() {
3331                                            let nx = nx.clone();
3332                                            self.advance();
3333                                            t.push(' ');
3334                                            t.push_str(&nx);
3335                                        }
3336                                        args.push(t);
3337                                    }
3338                                }
3339                            }
3340                            items.push((name, if star { None } else { Some(args) }));
3341                            if matches!(self.peek(), Token::Comma) {
3342                                self.advance();
3343                            } else {
3344                                break;
3345                            }
3346                        }
3347                        self.consume_until_statement_boundary();
3348                        Ok(Statement::DropAggregate { if_exists, items })
3349                    }
3350                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3351                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3352                    // installed; `IF EXISTS` is the spelling that says do
3353                    // not, and it keeps the no-op.
3354                    Token::Ident(s) | Token::QuotedIdent(s)
3355                        if s.eq_ignore_ascii_case("extension") =>
3356                    {
3357                        self.advance();
3358                        let if_exists = self.consume_if_exists();
3359                        let names = self.take_comma_separated_names();
3360                        self.consume_until_statement_boundary();
3361                        if if_exists {
3362                            return Ok(Statement::Empty);
3363                        }
3364                        Ok(Statement::ValidateOnly {
3365                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3366                            names,
3367                        })
3368                    }
3369                    Token::Ident(s) | Token::QuotedIdent(s)
3370                        if s.eq_ignore_ascii_case("statistics") =>
3371                    {
3372                        self.parse_drop_statistics_after_drop()
3373                    }
3374                    other => Err(self.err(format!(
3375                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3376                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3377                    ))),
3378                }
3379            }
3380            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3381            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3382            // and accepted before the view name. SPG materialised
3383            // views re-evaluate on read (always-fresh semantics), so
3384            // the CONCURRENTLY-vs-serial distinction has no runtime
3385            // effect — the refresh body does not block readers either
3386            // way. Same accept-and-no-op pattern as DETACH PARTITION
3387            // CONCURRENTLY (16.5).
3388            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3389                self.advance();
3390                let nxt = self.peek().clone();
3391                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3392                {
3393                    return Err(self.err(alloc::format!(
3394                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3395                    )));
3396                }
3397                self.advance();
3398                let nxt2 = self.peek().clone();
3399                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3400                {
3401                    return Err(self.err(alloc::format!(
3402                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3403                    )));
3404                }
3405                self.advance();
3406                // Optional CONCURRENTLY noise word — consumed without
3407                // changing semantics.
3408                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3409                {
3410                    self.advance();
3411                }
3412                let name = self.expect_ident_like()?;
3413                let with_data = self.parse_optional_with_data(true)?;
3414                Ok(Statement::RefreshMaterializedView { name, with_data })
3415            }
3416            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3417                self.advance();
3418                self.parse_update_after_keyword()
3419            }
3420            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3421            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3422            // [CASCADE | RESTRICT]. Clears every row from each named
3423            // table. Parses at the top level; the engine dispatcher
3424            // walks Statement::Truncate.
3425            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3426                self.advance();
3427                // Optional TABLE noise word — PG accepts both the reserved
3428                // token and the bare identifier spelling.
3429                if matches!(self.peek(), Token::Table)
3430                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3431                {
3432                    self.advance();
3433                }
3434                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3435                // not absorbed. The lookahead keeps a table genuinely
3436                // called `only` working: the keyword is a keyword only
3437                // when a name follows it.
3438                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3439                    if s.eq_ignore_ascii_case("only"))
3440                    && matches!(
3441                        self.tokens.get(self.pos + 1),
3442                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3443                    );
3444                if only {
3445                    self.advance();
3446                }
3447                // Table names (comma-separated).
3448                let mut tables = Vec::new();
3449                loop {
3450                    tables.push(self.expect_ident_like()?);
3451                    if matches!(self.peek(), Token::Comma) {
3452                        self.advance();
3453                        continue;
3454                    }
3455                    break;
3456                }
3457                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3458                let mut restart_identity = false;
3459                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3460                {
3461                    self.advance();
3462                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3463                    {
3464                        self.advance();
3465                        restart_identity = true;
3466                    }
3467                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3468                {
3469                    self.advance();
3470                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3471                    {
3472                        self.advance();
3473                    }
3474                }
3475                // Optional CASCADE / RESTRICT.
3476                let mut cascade = false;
3477                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3478                {
3479                    self.advance();
3480                    cascade = true;
3481                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3482                {
3483                    self.advance();
3484                }
3485                Ok(Statement::Truncate {
3486                    tables,
3487                    restart_identity,
3488                    cascade,
3489                    only,
3490                })
3491            }
3492            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3493            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3494            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3495            // rows change so the index tree is always up-to-date;
3496            // REINDEX is a strict no-op. Accept the whole statement
3497            // shape to boundary for pg_dump round-trip compatibility.
3498            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3499                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3500                // index bloat to rebuild, so the work stays a no-op, but PG
3501                // validates what it was pointed at and this swallowed the
3502                // name at parse time — `REINDEX TABLE typo` reported
3503                // success. Measured on PG18: INDEX / TABLE name a relation,
3504                // SCHEMA a schema, SYSTEM nothing.
3505                self.advance();
3506                self.parse_reindex_tail()
3507            }
3508            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3509            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3510            // SPG has no MVCC bloat today (Phase D visibility map
3511            // queues with v7.38); the freezer collapses hot-tier
3512            // rows into cold segments automatically. VACUUM is a
3513            // no-op — pg_dump maintenance scripts and Discourse's
3514            // periodic-maintenance path both emit it.
3515            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3516            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3517            // actual bloat, so the pre-MVCC accept-and-ignore posture
3518            // became a silent no-op on a customer's manual reclaim.
3519            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3520            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3521            // ANALYZE is captured, the optional table name is captured.
3522            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3523                self.advance();
3524                // Parenthesised option list: absorb it.
3525                if matches!(self.peek(), Token::LParen) {
3526                    let mut depth = 0usize;
3527                    loop {
3528                        match self.advance() {
3529                            Token::LParen => depth += 1,
3530                            Token::RParen => {
3531                                depth -= 1;
3532                                if depth == 0 {
3533                                    break;
3534                                }
3535                            }
3536                            Token::Eof => break,
3537                            _ => {}
3538                        }
3539                    }
3540                }
3541                let mut analyze = false;
3542                let mut table: Option<String> = None;
3543                loop {
3544                    match self.peek() {
3545                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3546                        // an identifier, so the loop below broke out on it and
3547                        // dropped the table name: `VACUUM FULL nosuch` was
3548                        // accepted where `VACUUM nosuch` was refused.
3549                        Token::Full => {
3550                            self.advance();
3551                        }
3552                        Token::Ident(w) | Token::QuotedIdent(w) => {
3553                            let wl = w.to_ascii_lowercase();
3554                            match wl.as_str() {
3555                                "full" | "freeze" | "verbose" => {
3556                                    self.advance();
3557                                }
3558                                "analyze" | "analyse" => {
3559                                    analyze = true;
3560                                    self.advance();
3561                                }
3562                                _ => {
3563                                    table = Some(self.expect_ident_like()?);
3564                                    break;
3565                                }
3566                            }
3567                        }
3568                        _ => break,
3569                    }
3570                }
3571                // Optional trailing column list / anything else to the
3572                // statement boundary (PG accepts per-column ANALYZE).
3573                self.consume_until_statement_boundary();
3574                Ok(Statement::Vacuum { table, analyze })
3575            }
3576            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3577            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3578            // <index>. PG stores rows in physical order matching
3579            // an index; SPG's hot-tier is append-only + cold-tier
3580            // is segment-frozen, so clustering has no persistent
3581            // effect. Accept-and-no-op for pg_dump compat.
3582            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3583                // v7.39 (round 535) — same as REINDEX above: the relation is
3584                // carried so the engine can refuse one that does not exist.
3585                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3586                self.advance();
3587                self.parse_cluster_tail()
3588            }
3589            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3590            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3591            // optional string payload; UNLISTEN takes a channel or `*`.
3592            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3593                self.advance();
3594                let ch = match self.advance() {
3595                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3596                    other => {
3597                        return Err(self.err(format!(
3598                            "expected channel name after LISTEN, got {other:?}"
3599                        )));
3600                    }
3601                };
3602                Ok(Statement::Listen(ch))
3603            }
3604            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3605                self.advance();
3606                let channel = match self.advance() {
3607                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3608                    other => {
3609                        return Err(self.err(format!(
3610                            "expected channel name after NOTIFY, got {other:?}"
3611                        )));
3612                    }
3613                };
3614                let payload = if matches!(self.peek(), Token::Comma) {
3615                    self.advance();
3616                    match self.advance() {
3617                        Token::String(p) => Some(p),
3618                        other => {
3619                            return Err(self.err(format!(
3620                                "expected string payload after NOTIFY <channel>, got {other:?}"
3621                            )));
3622                        }
3623                    }
3624                } else {
3625                    None
3626                };
3627                Ok(Statement::Notify { channel, payload })
3628            }
3629            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3630                self.advance();
3631                match self.advance() {
3632                    Token::Star => Ok(Statement::Unlisten(None)),
3633                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3634                    other => Err(self.err(format!(
3635                        "expected channel name or * after UNLISTEN, got {other:?}"
3636                    ))),
3637                }
3638            }
3639            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3640            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3641            // process-wide write lock today; explicit LOCK has no
3642            // effect. Accept-and-no-op for pg_dump / migration
3643            // compat.
3644            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3645                self.advance();
3646                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3647                // engine holds a process-wide write lock), but the TABLE
3648                // NAME is now carried out so the engine can refuse one that
3649                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3650                // READ|WRITE` is a different statement with the same first
3651                // word; it keeps the old no-op, because a MySQL dump's
3652                // bracket names tables it is about to create.
3653                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3654                    if k.eq_ignore_ascii_case("tables"));
3655                if mysql_tables {
3656                    self.consume_until_statement_boundary();
3657                    return Ok(Statement::Empty);
3658                }
3659                if matches!(self.peek(), Token::Table) {
3660                    self.advance();
3661                }
3662                let names = self.take_comma_separated_names();
3663                self.consume_until_statement_boundary();
3664                Ok(Statement::ValidateOnly {
3665                    kind: crate::ast::ValidateOnlyKind::LockTable,
3666                    names,
3667                })
3668            }
3669            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3670            // durability marker + snapshot in PG. SPG has WAL
3671            // checkpointing on a byte / time schedule (v7.37.10
3672            // 60s / 4 MiB defaults). The bare statement parses to
3673            // `Statement::Empty` here (the no_std engine owns no
3674            // WAL / snapshot); v7.37 Epic Du wires the HOST
3675            // (embedded `Database::execute_buffered`, via
3676            // `sql_is_checkpoint`) to force an immediate synchronous
3677            // checkpoint through `Database::checkpoint` — a real
3678            // durability barrier, matching PG.
3679            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3680                self.advance();
3681                self.consume_until_statement_boundary();
3682                Ok(Statement::Empty)
3683            }
3684            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3685                self.advance();
3686                self.parse_delete_after_keyword()
3687            }
3688            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3689            // ALTER is not a reserved keyword in the lexer — handled
3690            // as a bare ident here.
3691            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3692                self.advance();
3693                self.parse_alter_after_keyword()
3694            }
3695            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3696            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3697            // additions needed.
3698            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3699                self.advance();
3700                self.parse_wait_after_keyword()
3701            }
3702            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3703            // Bare ANALYZE → analyse every user table; ANALYZE
3704            // <name> → re-stats one. The argument is an optional
3705            // ident (or quoted ident); anything else is a parse
3706            // error.
3707            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3708            // `WHERE` filter (carved out per V6_7_DESIGN.md
3709            // STABILITY). Lex order: identifier "compact" → "cold"
3710            // → "segments". Anything else after `COMPACT` is a
3711            // parse error.
3712            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3713                self.advance();
3714                let next = self.peek().clone();
3715                let cold = match next {
3716                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3717                    _ => {
3718                        return Err(
3719                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3720                        );
3721                    }
3722                };
3723                if !cold.eq_ignore_ascii_case("cold") {
3724                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3725                }
3726                self.advance();
3727                let next = self.peek().clone();
3728                let segments = match next {
3729                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3730                    _ => {
3731                        return Err(self.err(format!(
3732                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3733                            self.peek()
3734                        )));
3735                    }
3736                };
3737                if !segments.eq_ignore_ascii_case("segments") {
3738                    return Err(self.err(format!(
3739                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3740                    )));
3741                }
3742                self.advance();
3743                Ok(Statement::CompactColdSegments)
3744            }
3745            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3746            // Parsed as a case-insensitive identifier since MERGE
3747            // isn't a reserved lexer keyword (collides with the
3748            // mysqldump `ALGORITHM = MERGE` view clause if it
3749            // were); the inner parser drives the rest of the
3750            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3751            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3752                self.advance();
3753                self.parse_merge_after_keyword()
3754            }
3755            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3756                self.advance();
3757                let target = match self.peek() {
3758                    Token::Eof | Token::Semicolon => None,
3759                    Token::Ident(_) | Token::QuotedIdent(_) => {
3760                        Some(self.expect_ident_like()?)
3761                    }
3762                    other => {
3763                        return Err(self.err(format!(
3764                            "expected table name or end of statement after ANALYZE, got {other:?}"
3765                        )));
3766                    }
3767                };
3768                // v7.39 (round 776, F31 J7) — the per-column form
3769                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3770                // here while the VACUUM arm already consumed it; SPG
3771                // analyzes whole tables, so the list parses and is
3772                // accepted like the VACUUM path's.
3773                if target.is_some() && matches!(self.peek(), Token::LParen) {
3774                    self.advance();
3775                    loop {
3776                        let _ = self.expect_ident_like()?;
3777                        match self.peek() {
3778                            Token::Comma => {
3779                                self.advance();
3780                            }
3781                            Token::RParen => {
3782                                self.advance();
3783                                break;
3784                            }
3785                            other => {
3786                                return Err(self.err(format!(
3787                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3788                                )));
3789                            }
3790                        }
3791                    }
3792                }
3793                Ok(Statement::Analyze(target))
3794            }
3795            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3796            // `default_text_search_config` parameter is consumed
3797            // by the FTS function dispatcher; other parameter
3798            // names are recorded but treated as a no-op so PG
3799            // dump output loads.
3800            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3801                self.advance();
3802                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3803                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3804                // …` which the SessionVar path handles). `LOCAL` is the only
3805                // one that changes semantics — it scopes the change to the
3806                // current transaction — so capture it; SESSION / GLOBAL are
3807                // accepted and treated as the default session scope.
3808                let mut set_local = false;
3809                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3810                    let q = s.to_ascii_lowercase();
3811                    if q == "local" || q == "session" || q == "global" {
3812                        set_local = q == "local";
3813                        self.advance();
3814                    }
3815                }
3816                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3817                // <collation>]` — change the connection client
3818                // charset. SPG stores UTF-8 always and orders
3819                // bytewise; accept as a no-op.
3820                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3821                {
3822                    self.advance();
3823                    // Charset ident-or-string.
3824                    if matches!(
3825                        self.peek(),
3826                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3827                    ) {
3828                        self.advance();
3829                    }
3830                    // Optional `COLLATE <name>`.
3831                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3832                    {
3833                        self.advance();
3834                        if matches!(
3835                            self.peek(),
3836                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3837                        ) {
3838                            self.advance();
3839                        }
3840                    }
3841                    return Ok(Statement::Empty);
3842                }
3843                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3844                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3845                // uses this to switch to the object owner before
3846                // recreating tables. SPG has no role system so this
3847                // is a no-op.
3848                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3849                {
3850                    self.advance(); // ROLE
3851                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3852                    // reset to the login identity; a name / string sets the
3853                    // effective role that drives current_user + RLS.
3854                    let role = match self.peek().clone() {
3855                        Token::Default => {
3856                            self.advance();
3857                            None
3858                        }
3859                        Token::Ident(s) | Token::QuotedIdent(s)
3860                            if s.eq_ignore_ascii_case("none") =>
3861                        {
3862                            self.advance();
3863                            None
3864                        }
3865                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3866                            self.advance();
3867                            Some(s)
3868                        }
3869                        _ => None,
3870                    };
3871                    return Ok(Statement::SetRole(role));
3872                }
3873                // v7.37.17 (17.6 sibling) — PG `SET SESSION
3874                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3875                // ISO SQL surface). pg_dump prepends this to fix
3876                // the isolation level for the restore session. SPG
3877                // defaults to READ COMMITTED and doesn't yet honor
3878                // session-set isolation across statements — accept
3879                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3880                // per-tx form is handled elsewhere.
3881                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3882                {
3883                    self.advance(); // CHARACTERISTICS
3884                    self.consume_until_statement_boundary();
3885                    return Ok(Statement::Empty);
3886                }
3887                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3888                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3889                // pg_dump emits this to control the deferrability of
3890                // FK / UNIQUE constraints across a bulk restore. SPG
3891                // has no deferrable-constraint machinery today; the
3892                // FK checker is strict-immediate. Accept-and-no-op
3893                // for pg_dump round-trip compatibility.
3894                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3895                {
3896                    self.advance(); // CONSTRAINTS
3897                    // v7.39 (round 288) — no longer a no-op: the trailing
3898                    // DEFERRED / IMMEDIATE sets the transaction's timing.
3899                    // v7.39 (round 308, V29) — and the names are kept.
3900                    // They used to be skipped over on the way to the
3901                    // DEFERRED keyword, so a named form silently behaved
3902                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3903                    // every deferrable constraint in the transaction.
3904                    let mut names: alloc::vec::Vec<alloc::string::String> =
3905                        alloc::vec::Vec::new();
3906                    if matches!(self.peek(), Token::All) {
3907                        self.advance();
3908                    } else {
3909                        loop {
3910                            let mut n = self.expect_ident_like()?;
3911                            // A schema-qualified name (`public.fk_a`)
3912                            // identifies the same constraint; PG resolves
3913                            // it by the trailing segment.
3914                            while matches!(self.peek(), Token::Dot) {
3915                                self.advance();
3916                                n = self.expect_ident_like()?;
3917                            }
3918                            names.push(n);
3919                            if matches!(self.peek(), Token::Comma) {
3920                                self.advance();
3921                            } else {
3922                                break;
3923                            }
3924                        }
3925                    }
3926                    let deferred = match self.peek() {
3927                        Token::Ident(s) | Token::QuotedIdent(s)
3928                            if s.eq_ignore_ascii_case("deferred") =>
3929                        {
3930                            true
3931                        }
3932                        Token::Ident(s) | Token::QuotedIdent(s)
3933                            if s.eq_ignore_ascii_case("immediate") =>
3934                        {
3935                            false
3936                        }
3937                        other => {
3938                            return Err(self.err(alloc::format!(
3939                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3940                            )));
3941                        }
3942                    };
3943                    self.advance();
3944                    return Ok(Statement::SetConstraints { names, deferred });
3945                }
3946                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3947                // { DEFAULT | '<role>' | <ident> }` (mailrs
3948                // round-10 A.1). pg_dump preamble emits the
3949                // `DEFAULT` form to reset session authorization.
3950                //
3951                // v7.39 (round 697) — this said "SPG has no role system so
3952                // this is a strict no-op". SPG has had one since round 58;
3953                // the comment outlived it, and with it the reason a name
3954                // that is not a role was accepted here. It still switches
3955                // no authorization — what it does now is refuse a role
3956                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3957                // AUTHORIZATION` (handled by the RESET parser
3958                // elsewhere). Reference:
3959                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3960                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3961                {
3962                    self.advance(); // AUTHORIZATION
3963                    match self.peek().clone() {
3964                        Token::Default => {
3965                            self.advance();
3966                        }
3967                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3968                            self.advance();
3969                            return Ok(Statement::ValidateOnly {
3970                                kind: crate::ast::ValidateOnlyKind::RoleName,
3971                                names: alloc::vec![r],
3972                            });
3973                        }
3974                        other => {
3975                            return Err(self.err(alloc::format!(
3976                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3977                            )));
3978                        }
3979                    }
3980                    return Ok(Statement::Empty);
3981                }
3982                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3983                // ISOLATION LEVEL { READ COMMITTED | READ
3984                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3985                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3986                // PG-standard surface. v7.37.8 accepts the syntax
3987                // and tracks the selected level on
3988                // `Engine::current_isolation_level()`; the actual
3989                // MVCC / SSI semantics implementation lands in
3990                // the 轴 4 isolation framework (separate train).
3991                // PG itself maps READ UNCOMMITTED to READ COMMITTED
3992                // internally; SPG behaves the same (effectively
3993                // READ COMMITTED at every level today).
3994                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3995                {
3996                    self.advance(); // TRANSACTION
3997                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3998                    return Ok(Statement::SetTransaction { isolation: level });
3999                }
4000                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4001                // alias — same accept-as-no-op as SET NAMES.
4002                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4003                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4004                {
4005                    self.advance(); // CHARACTER
4006                    self.advance(); // SET
4007                    if matches!(
4008                        self.peek(),
4009                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4010                    ) {
4011                        self.advance();
4012                    }
4013                    return Ok(Statement::Empty);
4014                }
4015                // v7.39 (GUC) — PG spells the timezone GUC as two
4016                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4017                // where <value> is a string/ident or the LOCAL /
4018                // DEFAULT keyword (both mean "back to the default").
4019                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4020                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4021                {
4022                    self.advance(); // TIME
4023                    self.advance(); // ZONE
4024                    let value = match self.peek().clone() {
4025                        Token::Ident(s)
4026                            if s.eq_ignore_ascii_case("local")
4027                                || s.eq_ignore_ascii_case("default") =>
4028                        {
4029                            self.advance();
4030                            crate::ast::SetValue::Default
4031                        }
4032                        Token::Default => {
4033                            self.advance();
4034                            crate::ast::SetValue::Default
4035                        }
4036                        _ => self.parse_set_value()?,
4037                    };
4038                    return Ok(Statement::SetParameter {
4039                        name: "timezone".into(),
4040                        value,
4041                        local: set_local,
4042                    });
4043                }
4044                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4045                // MySQL USER-variable assignment: its own per-session
4046                // namespace, an arbitrary expression on the right, and `:=`
4047                // as a second spelling of `=`. It used to fall into the
4048                // session-PARAMETER list below, whose values are literals and
4049                // whose store nothing reads back under a `@` name — so the
4050                // assignment reported success and vanished.
4051                //
4052                // A `@@`-prefixed LHS is a real engine setting and keeps the
4053                // old path.
4054                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4055                    return self.parse_set_user_vars();
4056                }
4057                // v7.14.0 — multi-assignment form
4058                // `SET a = 1, b = 2, …`. Single-assignment is the
4059                // 1-element case. Each LHS may be a regular ident
4060                // or a SessionVar (`@VAR` / `@@VAR`).
4061                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4062                loop {
4063                    let lhs = match self.peek().clone() {
4064                        Token::SessionVar(s) => {
4065                            self.advance();
4066                            s
4067                        }
4068                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4069                        other => {
4070                            return Err(self.err(format!(
4071                                "expected parameter name after SET, got {other:?}"
4072                            )));
4073                        }
4074                    };
4075                    // Accept either `=` or the bare `TO` keyword.
4076                    match self.peek() {
4077                        Token::Eq => {
4078                            self.advance();
4079                        }
4080                        Token::To => {
4081                            self.advance();
4082                        }
4083                        other => {
4084                            return Err(self.err(format!(
4085                                "expected `=` or TO after SET {lhs}, got {other:?}"
4086                            )));
4087                        }
4088                    }
4089                    let mut value = self.parse_set_value()?;
4090                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4091                    // `, name TO` continues a MySQL-style multi-assign,
4092                    // anything else is a PG list VALUE
4093                    // (`SET search_path = myschema, public`) folded into
4094                    // one comma-joined string.
4095                    while matches!(self.peek(), Token::Comma) {
4096                        let is_assign = matches!(
4097                            self.tokens.get(self.pos + 1),
4098                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4099                        ) && matches!(
4100                            self.tokens.get(self.pos + 2),
4101                            Some(Token::Eq | Token::To)
4102                        );
4103                        if is_assign {
4104                            break;
4105                        }
4106                        self.advance(); // comma
4107                        let next = self.parse_set_value()?;
4108                        let joined = alloc::format!(
4109                            "{}, {}",
4110                            set_value_text(&value),
4111                            set_value_text(&next)
4112                        );
4113                        value = crate::ast::SetValue::String(joined);
4114                    }
4115                    pairs.push((lhs, value));
4116                    if matches!(self.peek(), Token::Comma) {
4117                        self.advance();
4118                        continue;
4119                    }
4120                    break;
4121                }
4122                if pairs.len() == 1 {
4123                    let (name, value) = pairs.into_iter().next().unwrap();
4124                    Ok(Statement::SetParameter {
4125                        name,
4126                        value,
4127                        local: set_local,
4128                    })
4129                } else {
4130                    Ok(Statement::SetParameterList(pairs))
4131                }
4132            }
4133            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4134            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4135                self.advance();
4136                match self.peek().clone() {
4137                    Token::All => {
4138                        self.advance();
4139                        Ok(Statement::ResetParameter(None))
4140                    }
4141                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4142                        self.advance();
4143                        Ok(Statement::ResetParameter(None))
4144                    }
4145                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4146                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4147                        self.advance();
4148                        Ok(Statement::SetRole(None))
4149                    }
4150                    _ => {
4151                        let name = self.parse_set_param_name()?;
4152                        Ok(Statement::ResetParameter(Some(name)))
4153                    }
4154                }
4155            }
4156            // v7.39 (round 218) — server-side cursors.
4157            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4158            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4159            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4160            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4161                self.advance();
4162                match self.peek().clone() {
4163                    Token::All => {
4164                        self.advance();
4165                        Ok(Statement::CloseCursor { name: None })
4166                    }
4167                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4168                        self.advance();
4169                        Ok(Statement::CloseCursor { name: None })
4170                    }
4171                    Token::Ident(n) | Token::QuotedIdent(n) => {
4172                        self.advance();
4173                        Ok(Statement::CloseCursor { name: Some(n) })
4174                    }
4175                    other => Err(self.err(format!(
4176                        "expected cursor name or ALL after CLOSE, got {other:?}"
4177                    ))),
4178                }
4179            }
4180            other => Err(self.err(format!(
4181                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4182                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4183            ))),
4184        }
4185    }
4186
4187    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4188    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4189    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4190    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4191    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4192        self.advance(); // DECLARE
4193        let name = match self.advance() {
4194            Token::Ident(n) | Token::QuotedIdent(n) => n,
4195            other => {
4196                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4197            }
4198        };
4199        let mut scroll: Option<bool> = None;
4200        loop {
4201            match self.peek() {
4202                Token::Ident(s)
4203                    if s.eq_ignore_ascii_case("binary")
4204                        || s.eq_ignore_ascii_case("insensitive")
4205                        || s.eq_ignore_ascii_case("asensitive") =>
4206                {
4207                    self.advance();
4208                }
4209                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4210                    self.advance();
4211                    scroll = Some(true);
4212                }
4213                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4214                {
4215                    self.advance(); // NO
4216                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4217                        return Err(self.err(format!(
4218                            "expected SCROLL after NO in DECLARE, got {:?}",
4219                            self.peek()
4220                        )));
4221                    }
4222                    self.advance();
4223                    scroll = Some(false);
4224                }
4225                _ => break,
4226            }
4227        }
4228        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4229            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4230        }
4231        self.advance();
4232        let mut hold = false;
4233        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4234            self.advance();
4235            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4236                return Err(self.err(format!(
4237                    "expected HOLD after WITH in DECLARE, got {:?}",
4238                    self.peek()
4239                )));
4240            }
4241            self.advance();
4242            hold = true;
4243        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4244            self.advance();
4245            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4246                return Err(self.err(format!(
4247                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4248                    self.peek()
4249                )));
4250            }
4251            self.advance();
4252        }
4253        if !matches!(self.peek(), Token::For) {
4254            return Err(self.err(format!(
4255                "expected FOR before the cursor query, got {:?}",
4256                self.peek()
4257            )));
4258        }
4259        self.advance();
4260        let query = self.parse_one_statement()?;
4261        Ok(Statement::DeclareCursor {
4262            name,
4263            scroll,
4264            hold,
4265            query: alloc::boxed::Box::new(query),
4266        })
4267    }
4268
4269    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4270    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4271    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4272    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4273        use crate::ast::CursorDirection as D;
4274        self.advance(); // FETCH / MOVE
4275        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4276            let neg = if matches!(this.peek(), Token::Minus) {
4277                this.advance();
4278                true
4279            } else {
4280                false
4281            };
4282            match this.advance() {
4283                Token::Integer(v) => Ok(if neg { -v } else { v }),
4284                other => Err(this.err(format!("expected count, got {other:?}"))),
4285            }
4286        };
4287        let direction = match self.peek().clone() {
4288            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4289                self.advance();
4290                D::Next
4291            }
4292            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4293                self.advance();
4294                D::Prior
4295            }
4296            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4297                self.advance();
4298                D::First
4299            }
4300            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4301                self.advance();
4302                D::Last
4303            }
4304            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4305                self.advance();
4306                D::Absolute(signed_count(self)?)
4307            }
4308            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4309                self.advance();
4310                D::Relative(signed_count(self)?)
4311            }
4312            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4313                self.advance();
4314                match self.peek().clone() {
4315                    Token::All => {
4316                        self.advance();
4317                        D::All
4318                    }
4319                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4320                        self.advance();
4321                        D::All
4322                    }
4323                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4324                    _ => D::Next, // bare FORWARD = FORWARD 1
4325                }
4326            }
4327            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4328                self.advance();
4329                match self.peek().clone() {
4330                    Token::All => {
4331                        self.advance();
4332                        D::BackwardAll
4333                    }
4334                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4335                        self.advance();
4336                        D::BackwardAll
4337                    }
4338                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4339                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4340                }
4341            }
4342            Token::All => {
4343                self.advance();
4344                D::All
4345            }
4346            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4347                self.advance();
4348                D::All
4349            }
4350            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4351            // Bare `FETCH <name>` — direction defaults to NEXT.
4352            _ => D::Next,
4353        };
4354        // Optional FROM / IN.
4355        if matches!(self.peek(), Token::From)
4356            || matches!(self.peek(), Token::In)
4357            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4358        {
4359            self.advance();
4360        }
4361        let name = match self.advance() {
4362            Token::Ident(n) | Token::QuotedIdent(n) => n,
4363            other => {
4364                return Err(self.err(format!("expected cursor name, got {other:?}")));
4365            }
4366        };
4367        Ok(if is_move {
4368            Statement::MoveCursor { name, direction }
4369        } else {
4370            Statement::FetchCursor { name, direction }
4371        })
4372    }
4373
4374    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4375    /// [(kind, …)] ON <col>, … FROM <table>`.
4376    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4377        self.advance(); // STATISTICS
4378        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4379        let mut if_not_exists = false;
4380        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4381            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4382        {
4383            self.advance();
4384            self.advance();
4385            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4386                self.advance();
4387                if_not_exists = true;
4388            }
4389        }
4390        let name = self.expect_ident_like()?;
4391        let mut kinds = Vec::new();
4392        if matches!(self.peek(), Token::LParen) {
4393            self.advance();
4394            loop {
4395                let k = self.expect_ident_like()?;
4396                // PG stores the single letters; accept the spelled-out
4397                // names the SQL uses and record what PG records.
4398                kinds.push(match k.to_ascii_lowercase().as_str() {
4399                    "ndistinct" => String::from("d"),
4400                    "dependencies" => String::from("f"),
4401                    "mcv" => String::from("m"),
4402                    other => {
4403                        return Err(
4404                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4405                        );
4406                    }
4407                });
4408                match self.advance() {
4409                    Token::Comma => {}
4410                    Token::RParen => break,
4411                    other => {
4412                        return Err(self.err(alloc::format!(
4413                            "expected ',' or ')' in statistics kind list, got {other:?}"
4414                        )));
4415                    }
4416                }
4417            }
4418        }
4419        if !matches!(self.peek(), Token::On) {
4420            return Err(self.err(alloc::format!(
4421                "expected ON in CREATE STATISTICS, got {:?}",
4422                self.peek()
4423            )));
4424        }
4425        self.advance();
4426        let mut columns = Vec::new();
4427        loop {
4428            columns.push(self.expect_ident_like()?);
4429            if matches!(self.peek(), Token::Comma) {
4430                self.advance();
4431            } else {
4432                break;
4433            }
4434        }
4435        if !matches!(self.peek(), Token::From) {
4436            return Err(self.err(alloc::format!(
4437                "expected FROM in CREATE STATISTICS, got {:?}",
4438                self.peek()
4439            )));
4440        }
4441        self.advance();
4442        let table = self.expect_ident_like()?;
4443        Ok(Statement::CreateStatistics {
4444            name,
4445            if_not_exists,
4446            kinds,
4447            columns,
4448            table,
4449        })
4450    }
4451
4452    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4453    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4454    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4455    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4456    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4457    /// forward call.
4458    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4459        self.advance(); // TABLE
4460        let if_exists = self.consume_if_exists();
4461        let mut names: Vec<String> = Vec::new();
4462        loop {
4463            names.push(self.expect_ident_like()?);
4464            if matches!(self.peek(), Token::Comma) {
4465                self.advance();
4466                continue;
4467            }
4468            break;
4469        }
4470        if matches!(
4471            self.peek(),
4472            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4473                || s.eq_ignore_ascii_case("restrict")
4474        ) {
4475            self.advance();
4476        }
4477        Ok(Statement::DropTable { names, if_exists })
4478    }
4479
4480    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4481        self.advance(); // STATISTICS
4482        let mut if_exists = false;
4483        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4484            && matches!(self.tokens.get(self.pos + 1),
4485                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4486        {
4487            self.advance();
4488            self.advance();
4489            if_exists = true;
4490        }
4491        let name = self.expect_ident_like()?;
4492        Ok(Statement::DropStatistics { name, if_exists })
4493    }
4494
4495    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4496        debug_assert!(matches!(self.peek(), Token::Create));
4497        self.advance();
4498        match self.peek() {
4499            Token::Table => self.parse_create_table_stmt_after_create(),
4500            Token::Index => self.parse_create_index_stmt_after_create(false),
4501            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4502            // object now. It used to be consumed by the CREATE-noise
4503            // arm, so a pg_dump that declares extended statistics
4504            // restored silently without them and reflection showed
4505            // nothing.
4506            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4507                self.parse_create_statistics_after_create()
4508            }
4509            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4510            // The `UNIQUE` modifier turns a partial index into a
4511            // partial-uniqueness invariant (only rows matching the
4512            // WHERE predicate are checked for duplicates). mailrs
4513            // K1 (3 hits: email_templates default, calendar_events
4514            // master, calendar_events instance).
4515            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4516                self.advance();
4517                if !matches!(self.peek(), Token::Index) {
4518                    return Err(self.err(alloc::format!(
4519                        "expected INDEX after CREATE UNIQUE, got {:?}",
4520                        self.peek()
4521                    )));
4522                }
4523                self.parse_create_index_stmt_after_create(true)
4524            }
4525            Token::Publication => {
4526                self.advance();
4527                self.parse_create_publication_after_keyword()
4528            }
4529            Token::Subscription => {
4530                self.advance();
4531                self.parse_create_subscription_after_keyword()
4532            }
4533            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4534            // USER isn't a reserved keyword — we look for the bare
4535            // identifier so the lexer doesn't have to grow a token.
4536            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4537                self.advance();
4538                self.parse_create_user_after_keyword(true)
4539            }
4540            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4541            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4542            // the default of the LOGIN attribute.
4543            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4544                self.advance();
4545                self.parse_create_user_after_keyword(false)
4546            }
4547            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4548            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4549                self.advance();
4550                self.parse_create_policy_after_keyword()
4551            }
4552            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4553            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4554            // no-op. mailrs follow-up F3.
4555            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4556                self.advance();
4557                self.parse_create_extension_after_keyword()
4558            }
4559            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4560            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4561            // optional; absorb it here and forward to the
4562            // per-kind parsers with the flag. OR is a reserved
4563            // keyword token.
4564            Token::Or => {
4565                self.advance();
4566                let next = self.peek();
4567                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4568                    return Err(self.err(alloc::format!(
4569                        "expected REPLACE after CREATE OR, got {next:?}"
4570                    )));
4571                };
4572                if !s2.eq_ignore_ascii_case("replace") {
4573                    return Err(self.err(alloc::format!(
4574                        "expected REPLACE after CREATE OR, got {s2:?}"
4575                    )));
4576                }
4577                self.advance();
4578                self.parse_create_function_or_trigger_after_or_replace(true)
4579            }
4580            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4581                self.advance();
4582                self.parse_create_function_after_keyword(false)
4583            }
4584            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4585                self.advance();
4586                self.parse_create_trigger_after_keyword(false)
4587            }
4588            // v7.39 (round 139) — CREATE RULE …
4589            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4590                self.advance();
4591                self.parse_create_rule_after_keyword(false)
4592            }
4593            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4594            // trigger is a row-level AFTER trigger that additionally carries
4595            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4596            // path already tolerates and skips those clauses, so consuming the
4597            // CONSTRAINT keyword and reusing it makes the statement parse and the
4598            // trigger fire. (The deferral timing itself is not yet honoured —
4599            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4600            // for every non-deferred use.)
4601            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4602                self.advance();
4603                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4604                    if t.eq_ignore_ascii_case("trigger"))
4605                {
4606                    return Err(self.err(alloc::format!(
4607                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4608                        self.peek()
4609                    )));
4610                }
4611                self.advance();
4612                self.parse_create_trigger_after_keyword(false)
4613            }
4614            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4615            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4616                self.advance();
4617                self.parse_create_sequence_after_keyword(false)
4618            }
4619            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4620            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4621                self.advance();
4622                self.parse_create_view_after_keyword(false, false, false)
4623            }
4624            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4625            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4626            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4627            // appear (in any order) between `CREATE` and `VIEW` in
4628            // every mysqldump-emitted view. Pre-2.6 the parser
4629            // rejected the prefix and the customer's whole view
4630            // backup failed on the first view. The hints are pure
4631            // planner / permission metadata; SPG's view-rewrite
4632            // path is semantically equivalent for all three
4633            // algorithms in v7.17 (TEMPTABLE differs only in
4634            // perf for huge views — out of v7.17 scope), and
4635            // DEFINER / SQL SECURITY are pure single-user
4636            // permissioning that SPG ignores by design.
4637            Token::Ident(s) | Token::QuotedIdent(s)
4638                if s.eq_ignore_ascii_case("algorithm")
4639                    || s.eq_ignore_ascii_case("definer")
4640                    || s.eq_ignore_ascii_case("sql") =>
4641            {
4642                self.consume_mysql_view_prefix()?;
4643                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4644                // (in any order, in any combination), the next
4645                // keyword must be VIEW. mysqldump never emits these
4646                // prefixes on non-view statements.
4647                let next = self.peek().clone();
4648                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4649                    if s2.eq_ignore_ascii_case("view"))
4650                {
4651                    self.advance();
4652                    self.parse_create_view_after_keyword(false, false, false)
4653                } else {
4654                    Err(self.err(alloc::format!(
4655                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4656                    )))
4657                }
4658            }
4659            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4660            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4661                self.advance();
4662                self.parse_create_type_after_keyword()
4663            }
4664            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4665            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4666            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4667                self.advance();
4668                self.parse_create_domain_after_keyword()
4669            }
4670            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4671            // name [AUTHORIZATION user]. Real catalog registry
4672            // (was silent-no-op'd pre-v7.17).
4673            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4674                self.advance();
4675                let if_not_exists = self.parse_if_not_exists();
4676                let name = self.expect_ident_like()?;
4677                // Optional `AUTHORIZATION <user>` trailer — accepted,
4678                // ignored (single-user catalog).
4679                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4680                    if s.eq_ignore_ascii_case("authorization"))
4681                {
4682                    self.advance();
4683                    let _ = self.expect_ident_like()?;
4684                }
4685                Ok(Statement::CreateSchema { name, if_not_exists })
4686            }
4687            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4688            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4689                self.advance();
4690                let next = self.peek().clone();
4691                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4692                {
4693                    self.advance();
4694                    self.parse_create_materialized_view_after_keyword()
4695                } else {
4696                    Err(self.err(alloc::format!(
4697                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4698                    )))
4699                }
4700            }
4701            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4702            // no-op below), an UNLOGGED table is a real, fully-usable table in
4703            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4704            // durability optimisation is a follow-up), so a dump / app that
4705            // declares UNLOGGED tables works instead of failing to parse.
4706            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4707                self.advance(); // UNLOGGED
4708                if matches!(self.peek(), Token::Table) {
4709                    self.parse_create_table_stmt_after_create()
4710                } else {
4711                    Err(self.err(format!(
4712                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4713                        self.peek()
4714                    )))
4715                }
4716            }
4717            Token::Ident(s) | Token::QuotedIdent(s)
4718                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4719            {
4720                self.advance();
4721                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4722                let next = self.peek().clone();
4723                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4724                {
4725                    self.advance();
4726                    self.parse_create_sequence_after_keyword(true)
4727                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4728                {
4729                    self.advance();
4730                    self.parse_create_view_after_keyword(false, false, true)
4731                } else {
4732                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4733                    // consumed and answered OK while creating nothing, so
4734                    // every statement that touched the table afterwards failed
4735                    // with "table not found" — the DDL itself lied. It is a
4736                    // real CREATE TABLE now, marked temporary so the executor
4737                    // puts it in the session's own namespace. An optional
4738                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4739                    // is not legal, but the keyword is consumed by the
4740                    // CREATE TABLE parser itself).
4741                    let stmt = self.parse_create_table_stmt_after_create()?;
4742                    match stmt {
4743                        Statement::CreateTable(mut c) => {
4744                            c.temporary = true;
4745                            Ok(Statement::CreateTable(c))
4746                        }
4747                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4748                        // CTAS node, which needs the same session namespace.
4749                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4750                            m.temporary = true;
4751                            Ok(Statement::CreateMaterializedView(m))
4752                        }
4753                        other => Ok(other),
4754                    }
4755                }
4756            }
4757            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4758            // BEGIN <body> END`. The body may reference `@var`
4759            // session variables, SET statements, internal `;`
4760            // terminators, etc. SPG has no procedure runtime, so
4761            // consume the whole `CREATE PROCEDURE … END` block as
4762            // a no-op so mysqldump scripts that include stored
4763            // routines load through. The matching-END consumer
4764            // tracks BEGIN/END nesting depth to handle nested
4765            // BEGIN blocks correctly.
4766            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4767                self.consume_mysql_routine_body();
4768                Ok(Statement::Empty)
4769            }
4770            // v7.14.0 — pg_dump / mysqldump emit
4771            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4772            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4773            // SPG is single-schema / single-database; these have
4774            // no behavioural effect, so consume + return Empty.
4775            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4776            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4777            // moved up to real parser branches. DATABASE / ROLE /
4778            // POLICY / OPERATOR stay no-op forever
4779            // (single-database, hardcoded roles).
4780            Token::Ident(s) | Token::QuotedIdent(s)
4781                if matches!(
4782                    s.to_ascii_lowercase().as_str(),
4783                    "database"
4784                        | "role"
4785                        | "operator"
4786                        | "cast"
4787                        | "aggregate"
4788                        | "language"
4789                        | "collation"
4790                        | "conversion"
4791                        // v7.17.0 Phase 8 (audit N6) — rarely-
4792                        // emitted pg_dump shapes that should
4793                        // load through without a parser error.
4794                        // SPG has no planner statistics catalog,
4795                        // no event-trigger hooks, no foreign-
4796                        // data-wrapper infrastructure; consume
4797                        // + return Empty.
4798                        | "statistics"
4799                        | "event"
4800                        // v7.37.17 (17.6 siblings) — additional CREATE
4801                        // targets pg_dump / operator install scripts
4802                        // may emit that SPG has no matching machinery
4803                        // for. Consume + Empty-return.
4804                        | "text"
4805                        | "tablespace"
4806                        | "access"
4807                        | "large"
4808                ) =>
4809            {
4810                // DATABASE is the one member of this list PG refuses
4811                // inside a transaction block; the rest (ROLE, CAST,
4812                // TABLESPACE, …) it runs there quite happily, so only
4813                // this one is named. Still a no-op otherwise — SPG is
4814                // single-database.
4815                let is_database = s.eq_ignore_ascii_case("database");
4816                self.consume_until_statement_boundary();
4817                if is_database {
4818                    return Ok(Statement::NoOpPreventedInTransaction {
4819                        what: String::from("CREATE DATABASE"),
4820                    });
4821                }
4822                Ok(Statement::Empty)
4823            }
4824            // v7.39 (round 706) — the foreign-data family leaves the silent
4825            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4826            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4827            // FDW machinery), but the ENGINE now warns, so a restore log
4828            // says what will not function instead of reporting success.
4829            Token::Ident(s) | Token::QuotedIdent(s)
4830                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4831            {
4832                self.consume_until_statement_boundary();
4833                Ok(Statement::ValidateOnly {
4834                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4835                    names: Vec::new(),
4836                })
4837            }
4838            other => Err(self.err(format!(
4839                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4840            ))),
4841        }
4842    }
4843
4844    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4845    /// keyword decides whether we parse a function or trigger
4846    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4847    /// PROCEDURE) — those land in later releases.
4848    fn parse_create_function_or_trigger_after_or_replace(
4849        &mut self,
4850        or_replace: bool,
4851    ) -> Result<Statement, ParseError> {
4852        let tok = self.peek();
4853        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4854            return Err(self.err(alloc::format!(
4855                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4856            )));
4857        };
4858        if s.eq_ignore_ascii_case("function") {
4859            self.advance();
4860            self.parse_create_function_after_keyword(or_replace)
4861        } else if s.eq_ignore_ascii_case("trigger") {
4862            self.advance();
4863            self.parse_create_trigger_after_keyword(or_replace)
4864        } else if s.eq_ignore_ascii_case("rule") {
4865            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4866            self.advance();
4867            self.parse_create_rule_after_keyword(or_replace)
4868        } else if s.eq_ignore_ascii_case("view") {
4869            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4870            self.advance();
4871            self.parse_create_view_after_keyword(or_replace, false, false)
4872        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4873            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4874            self.advance();
4875            let nxt = self.peek().clone();
4876            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4877            {
4878                self.advance();
4879                self.parse_create_view_after_keyword(or_replace, false, true)
4880            } else {
4881                Err(self.err(alloc::format!(
4882                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4883                )))
4884            }
4885        } else {
4886            Err(self.err(alloc::format!(
4887                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4888            )))
4889        }
4890    }
4891
4892    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4893    /// SPG doesn't have a registry; pgvector / similar are
4894    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4895    /// the syntax lets dual-target schemas keep the line.
4896    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4897        // Optional `IF NOT EXISTS`.
4898        self.consume_if_not_exists();
4899        let name = self.expect_ident_like()?;
4900        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4901        // CASCADE / FROM '<v>' clauses; we don't model them.
4902        loop {
4903            match self.peek() {
4904                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4905                    self.advance();
4906                    continue;
4907                }
4908                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4909                    self.advance();
4910                    let _ = self.expect_ident_like()?;
4911                    continue;
4912                }
4913                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4914                    self.advance();
4915                    // String or ident literal.
4916                    let _ = self.advance();
4917                    continue;
4918                }
4919                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4920                    self.advance();
4921                    let _ = self.advance();
4922                    continue;
4923                }
4924                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4925                    self.advance();
4926                    continue;
4927                }
4928                _ => break,
4929            }
4930        }
4931        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4932        // nosuch` reported success and `pg_extension` then did not list it,
4933        // which is the accept-and-do-nothing shape F31 exists to find.
4934        Ok(Statement::ValidateOnly {
4935            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4936            names: alloc::vec![name],
4937        })
4938    }
4939
4940    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4941    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4942    /// already been consumed by the caller. Grammar accepted:
4943    ///
4944    ///   name `(` arg-list `)`
4945    ///   `RETURNS` return-type
4946    ///   [ `LANGUAGE` ident ]
4947    ///   `AS` $$ body $$
4948    ///   [ `LANGUAGE` ident ]
4949    ///
4950    /// Either `LANGUAGE` position is allowed; PG accepts both.
4951    fn parse_create_function_after_keyword(
4952        &mut self,
4953        or_replace: bool,
4954    ) -> Result<Statement, ParseError> {
4955        let name = self.expect_ident_like()?;
4956        // Argument list. v7.12.4 commonly sees the empty `()`
4957        // (trigger functions); typed args parse and round-trip
4958        // but the executor only invokes nullary functions.
4959        if !matches!(self.peek(), Token::LParen) {
4960            return Err(self.err(alloc::format!(
4961                "expected '(' after function name {name:?}, got {:?}",
4962                self.peek()
4963            )));
4964        }
4965        self.advance();
4966        let args = self.parse_function_arg_list()?;
4967        // RETURNS clause.
4968        let tok = self.peek();
4969        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4970            return Err(self.err(alloc::format!(
4971                "expected RETURNS after function arg list, got {tok:?}"
4972            )));
4973        };
4974        if !s.eq_ignore_ascii_case("returns") {
4975            return Err(self.err(alloc::format!(
4976                "expected RETURNS after function arg list, got {s:?}"
4977            )));
4978        }
4979        self.advance();
4980        let returns = self.parse_function_return()?;
4981        // Optional LANGUAGE clause (PG also accepts after AS — we'll
4982        // re-check after the body too).
4983        let mut language: Option<String> = self.parse_optional_language()?;
4984        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4985        // either side of the body and in any order, interleaved with
4986        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4987        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4988        // PG's own pg_dump output did not restore.
4989        let mut attrs = FunctionAttrs::default();
4990        loop {
4991            let before = self.pos;
4992            self.parse_function_attrs_into(&mut attrs)?;
4993            if language.is_none() {
4994                language = self.parse_optional_language()?;
4995            }
4996            if self.pos == before {
4997                break;
4998            }
4999        }
5000        // `AS` followed by a $$-quoted body (lexer already
5001        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5002        // Token::String). AS is a reserved keyword (Token::As).
5003        if !matches!(self.peek(), Token::As) {
5004            return Err(self.err(alloc::format!(
5005                "expected AS before function body, got {:?}",
5006                self.peek()
5007            )));
5008        }
5009        self.advance();
5010        let body_text = match self.peek() {
5011            Token::String(s) => {
5012                let body = s.clone();
5013                self.advance();
5014                body
5015            }
5016            other => {
5017                return Err(self.err(alloc::format!(
5018                    "expected $$-quoted function body after AS, got {other:?}"
5019                )));
5020            }
5021        };
5022        // Trailing clauses — PG's other accepted position for both the
5023        // LANGUAGE and the attributes.
5024        loop {
5025            let before = self.pos;
5026            self.parse_function_attrs_into(&mut attrs)?;
5027            if language.is_none() {
5028                language = self.parse_optional_language()?;
5029            }
5030            if self.pos == before {
5031                break;
5032            }
5033        }
5034        let language = language.unwrap_or_else(|| String::from("sql"));
5035        // PL/pgSQL bodies get structure-parsed. Other languages
5036        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5037        // recognise) round-trip as Raw text — the executor errors
5038        // when invoked with a clear unsupported message.
5039        let body = if language.eq_ignore_ascii_case("plpgsql") {
5040            match parse_plpgsql_body(&body_text) {
5041                Ok(block) => FunctionBody::PlPgSql(block),
5042                // Best-effort: if the body parser doesn't yet
5043                // support a construct used inside, fall back to
5044                // raw — keeps `CREATE FUNCTION` itself working
5045                // (catalogue accepts), executor errors on
5046                // invocation only.
5047                Err(_) => FunctionBody::Raw(body_text),
5048            }
5049        } else {
5050            FunctionBody::Raw(body_text)
5051        };
5052        Ok(Statement::CreateFunction(CreateFunctionStatement {
5053            name,
5054            or_replace,
5055            args,
5056            returns,
5057            language,
5058            body,
5059            attrs,
5060        }))
5061    }
5062
5063    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5064    /// attribute clauses into `attrs`, stopping at the first token that
5065    /// is not one. Measured against PG 18.4, which accepts them in any
5066    /// order and on either side of the body.
5067    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5068        loop {
5069            let word = match self.peek() {
5070                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5071                // NOT LEAKPROOF — NOT is a reserved keyword token.
5072                Token::Not
5073                    if matches!(
5074                        self.tokens.get(self.pos + 1),
5075                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5076                    ) =>
5077                {
5078                    self.advance();
5079                    self.advance();
5080                    attrs.leakproof = false;
5081                    continue;
5082                }
5083                _ => return Ok(()),
5084            };
5085            match word.as_str() {
5086                "immutable" => {
5087                    self.advance();
5088                    attrs.volatility = FunctionVolatility::Immutable;
5089                }
5090                "stable" => {
5091                    self.advance();
5092                    attrs.volatility = FunctionVolatility::Stable;
5093                }
5094                "volatile" => {
5095                    self.advance();
5096                    attrs.volatility = FunctionVolatility::Volatile;
5097                }
5098                "strict" => {
5099                    self.advance();
5100                    attrs.strict = true;
5101                }
5102                "leakproof" => {
5103                    self.advance();
5104                    attrs.leakproof = true;
5105                }
5106                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5107                // spelled-out forms of STRICT and its opposite.
5108                "returns" | "called" => {
5109                    let strict = word == "returns";
5110                    let mut probe = self.pos + 1;
5111                    if strict {
5112                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5113                        // is not ours.
5114                        match self.tokens.get(probe) {
5115                            Some(Token::Null) => probe += 1,
5116                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5117                            _ => return Ok(()),
5118                        }
5119                    }
5120                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5121                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5122                    if !ok {
5123                        return Ok(());
5124                    }
5125                    probe += 1;
5126                    match self.tokens.get(probe) {
5127                        Some(Token::Null) => probe += 1,
5128                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5129                        _ => return Ok(()),
5130                    }
5131                    match self.tokens.get(probe) {
5132                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5133                        _ => return Ok(()),
5134                    }
5135                    self.pos = probe;
5136                    attrs.strict = strict;
5137                }
5138                "security" | "external" => {
5139                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5140                    let mut probe = self.pos + 1;
5141                    if word == "external" {
5142                        match self.tokens.get(probe) {
5143                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5144                                probe += 1;
5145                            }
5146                            _ => return Ok(()),
5147                        }
5148                    }
5149                    let definer = match self.tokens.get(probe) {
5150                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5151                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5152                        _ => return Ok(()),
5153                    };
5154                    self.pos = probe + 1;
5155                    attrs.security_definer = definer;
5156                }
5157                "parallel" => {
5158                    let level = match self.tokens.get(self.pos + 1) {
5159                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5160                            FunctionParallel::Safe
5161                        }
5162                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5163                            FunctionParallel::Restricted
5164                        }
5165                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5166                            FunctionParallel::Unsafe
5167                        }
5168                        _ => return Ok(()),
5169                    };
5170                    self.pos += 2;
5171                    attrs.parallel = level;
5172                }
5173                "cost" | "rows" => {
5174                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5175                        return Ok(());
5176                    };
5177                    self.pos += 2;
5178                    if word == "cost" {
5179                        attrs.cost = Some(n);
5180                    } else {
5181                        attrs.rows = Some(n);
5182                    }
5183                }
5184                _ => return Ok(()),
5185            }
5186        }
5187    }
5188
5189    /// The numeric literal at `idx`, if there is one.
5190    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5191        match self.tokens.get(idx)? {
5192            Token::Integer(n) => Some(*n as f64),
5193            Token::Float(f) => Some(*f),
5194            Token::Numeric(t) => t.parse::<f64>().ok(),
5195            _ => None,
5196        }
5197    }
5198
5199    /// Closing `)`-terminated argument list. v7.12.4 commonly
5200    /// sees the empty `()`; typed args round-trip but the
5201    /// executor (yet) doesn't invoke them.
5202    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5203    /// it away, which is what PG does with one on a function parameter.
5204    fn skip_type_modifier(&mut self) {
5205        if !matches!(self.peek(), Token::LParen) {
5206            return;
5207        }
5208        // Only a numeric modifier — anything else is not one, and eating
5209        // it would swallow real grammar.
5210        let mut i = self.pos + 1;
5211        let mut seen_number = false;
5212        loop {
5213            match self.tokens.get(i) {
5214                Some(Token::Integer(_)) => seen_number = true,
5215                Some(Token::Comma) => {}
5216                Some(Token::RParen) => break,
5217                _ => return,
5218            }
5219            i += 1;
5220        }
5221        if !seen_number {
5222            return;
5223        }
5224        while self.pos <= i {
5225            self.advance();
5226        }
5227    }
5228
5229    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5230        let mut args: Vec<FunctionArg> = Vec::new();
5231        if matches!(self.peek(), Token::RParen) {
5232            self.advance();
5233            return Ok(args);
5234        }
5235        loop {
5236            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5237            // a reserved token; OUT / INOUT are bare idents.
5238            let mode = if matches!(self.peek(), Token::In) {
5239                self.advance();
5240                FunctionArgMode::In
5241            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5242            {
5243                self.advance();
5244                FunctionArgMode::Out
5245            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5246            {
5247                self.advance();
5248                FunctionArgMode::InOut
5249            } else {
5250                FunctionArgMode::In
5251            };
5252            // Optional name. The next token is either a name
5253            // (followed by a type ident) or the type itself.
5254            // Disambiguate by peeking ahead: if the token after
5255            // the next ident is also an ident, we treat the
5256            // first as the name.
5257            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5258            // the comma or paren, then decide. Reading at most two of
5259            // them could not spell `x double precision` at all, and
5260            // silently mis-read the bare `double precision` as a
5261            // parameter named "double" — which is what made the same
5262            // signature key two different ways.
5263            let (name, ty_token) = {
5264                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5265                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5266                    words.push(self.expect_ident_like()?);
5267                }
5268                // v7.39 (round 344) — a length / precision modifier on the
5269                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5270                // accepts it and DROPS it — `pg_get_function_arguments`
5271                // reports plain `character varying` / `numeric`, measured on
5272                // 18.4 — but SPG raised `syntax error at or near "("`,
5273                // because the modifier's parens were never consumed.
5274                self.skip_type_modifier();
5275                // r1049 — `f(v bigint[])`. The array suffix parsed in
5276                // the column position, the cast position and (r1038)
5277                // the RETURNS position, but not here: the fifth
5278                // member of the same family, reported by sentori as
5279                // presumably the same code. It is now.
5280                let array_suffix = self.consume_array_suffix();
5281                let whole = words.join(" ");
5282                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5283                {
5284                    (Some(words[0].clone()), words[1..].join(" "))
5285                } else {
5286                    (None, whole)
5287                };
5288                ty_token.push_str(&array_suffix);
5289                (name, ty_token)
5290            };
5291            // Type — try to map to ColumnTypeName, else Raw.
5292            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5293                Some(t) => FunctionArgType::Typed(t),
5294                None => FunctionArgType::Raw(ty_token),
5295            };
5296            args.push(FunctionArg { mode, name, ty });
5297            match self.peek() {
5298                Token::Comma => {
5299                    self.advance();
5300                    continue;
5301                }
5302                Token::RParen => {
5303                    self.advance();
5304                    return Ok(args);
5305                }
5306                other => {
5307                    return Err(self.err(alloc::format!(
5308                        "expected , or ) in function arg list, got {other:?}"
5309                    )));
5310                }
5311            }
5312        }
5313    }
5314
5315    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5316        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5317        // function whose row shape is named inline.
5318        if matches!(self.peek(), Token::Table)
5319            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5320        {
5321            self.advance(); // TABLE
5322            self.advance(); // (
5323            let mut cols: Vec<String> = Vec::new();
5324            loop {
5325                let cname = self.expect_ident_like()?;
5326                let mut ty: Vec<String> = Vec::new();
5327                loop {
5328                    match self.peek() {
5329                        Token::Comma | Token::RParen | Token::Eof => break,
5330                        _ => {}
5331                    }
5332                    match self.advance() {
5333                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5334                        other => {
5335                            if let Some(w) = unreserved_keyword_text(&other) {
5336                                ty.push(w);
5337                            }
5338                        }
5339                    }
5340                }
5341                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5342                if matches!(self.peek(), Token::Comma) {
5343                    self.advance();
5344                } else {
5345                    break;
5346                }
5347            }
5348            if matches!(self.peek(), Token::RParen) {
5349                self.advance();
5350            }
5351            return Ok(FunctionReturn::Other(alloc::format!(
5352                "TABLE({})",
5353                cols.join(", ")
5354            )));
5355        }
5356        let ident = self.expect_ident_like()?;
5357        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5358        if ident.eq_ignore_ascii_case("setof") {
5359            let inner = self.expect_ident_like()?;
5360            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5361            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5362        }
5363        if ident.eq_ignore_ascii_case("trigger") {
5364            return Ok(FunctionReturn::Trigger);
5365        }
5366        if ident.eq_ignore_ascii_case("void") {
5367            return Ok(FunctionReturn::Void);
5368        }
5369        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5370        // RETURN position did not, so the `[` was a syntax error and the
5371        // whole migration stopped. sentori worked around it by returning
5372        // zero-padded text.
5373        let suffix = self.consume_array_suffix();
5374        if !suffix.is_empty() {
5375            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5376        }
5377        match map_type_ident_to_column_type_name(&ident) {
5378            Some(t) => Ok(FunctionReturn::Type(t)),
5379            None => Ok(FunctionReturn::Other(ident)),
5380        }
5381    }
5382
5383    /// Consume any `[]` / `[N]` array markers after a type name and give
5384    /// back their text. Empty when there are none.
5385    fn consume_array_suffix(&mut self) -> String {
5386        let mut out = String::new();
5387        while matches!(self.peek(), Token::LBracket) {
5388            self.advance();
5389            // `[N]` is accepted and, as in PG, the length is not enforced.
5390            if let Token::Integer(n) = self.peek().clone() {
5391                self.advance();
5392                out.push_str(&alloc::format!("[{n}]"));
5393            } else {
5394                out.push_str("[]");
5395            }
5396            if matches!(self.peek(), Token::RBracket) {
5397                self.advance();
5398            }
5399        }
5400        out
5401    }
5402
5403    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5404        match self.peek() {
5405            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5406                self.advance();
5407                let lang = self.expect_ident_like()?;
5408                Ok(Some(lang.to_ascii_lowercase()))
5409            }
5410            _ => Ok(None),
5411        }
5412    }
5413
5414    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5415    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5416    /// (expr)]*`. The `DOMAIN` keyword has already been
5417    /// consumed. PG allows the trailing constraints in any
5418    /// order; we approximate with a small loop.
5419    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5420        let name = self.expect_ident_like()?;
5421        // Optional `AS`.
5422        if matches!(self.peek(), Token::As) {
5423            self.advance();
5424        }
5425        // v7.39 (round 259) — keep the raw type NAME when the base is not
5426        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5427        // parent domain.
5428        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5429            self.parse_type_with_implied_flags()?;
5430        let mut default: Option<Expr> = None;
5431        let mut not_null = false;
5432        let mut checks: Vec<Expr> = Vec::new();
5433        loop {
5434            match self.peek() {
5435                Token::Default => {
5436                    if default.is_some() {
5437                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5438                    }
5439                    self.advance();
5440                    default = Some(self.parse_expr(0)?);
5441                }
5442                Token::Not => {
5443                    self.advance();
5444                    if !matches!(self.peek(), Token::Null) {
5445                        return Err(self.err(alloc::format!(
5446                            "expected NULL after NOT in DOMAIN, got {:?}",
5447                            self.peek()
5448                        )));
5449                    }
5450                    self.advance();
5451                    not_null = true;
5452                }
5453                Token::Null => {
5454                    self.advance();
5455                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5456                    // is the default-nullable marker (PG accepts it),
5457                    // but AFTER a NOT NULL it is a conflict PG refuses
5458                    // (`conflicting NULL/NOT NULL constraints`,
5459                    // PG18-measured); the old arm no-opped both ways.
5460                    if not_null {
5461                        return Err(self.err(alloc::string::String::from(
5462                            "conflicting NULL/NOT NULL constraints",
5463                        )));
5464                    }
5465                }
5466                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5467                    self.advance();
5468                    if !matches!(self.peek(), Token::LParen) {
5469                        return Err(self.err(alloc::format!(
5470                            "expected '(' after CHECK in DOMAIN, got {:?}",
5471                            self.peek()
5472                        )));
5473                    }
5474                    self.advance();
5475                    let expr = self.parse_expr(0)?;
5476                    if !matches!(self.peek(), Token::RParen) {
5477                        return Err(self.err(alloc::format!(
5478                            "expected ')' after CHECK expr, got {:?}",
5479                            self.peek()
5480                        )));
5481                    }
5482                    self.advance();
5483                    checks.push(expr);
5484                }
5485                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5486                // prefix on the constraint; we drop the name and
5487                // recurse into the constraint parsing.
5488                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5489                    self.advance();
5490                    let _ = self.expect_ident_like()?;
5491                }
5492                _ => break,
5493            }
5494        }
5495        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5496            name,
5497            base_type,
5498            base_domain: base_user_ref,
5499            default,
5500            not_null,
5501            checks,
5502        }))
5503    }
5504
5505    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5506    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5507    /// consumed.
5508    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5509        let name = self.expect_ident_like()?;
5510        // Required `AS`.
5511        if !matches!(self.peek(), Token::As) {
5512            return Err(self.err(alloc::format!(
5513                "expected AS after CREATE TYPE {name:?}, got {:?}",
5514                self.peek()
5515            )));
5516        }
5517        self.advance();
5518        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5519        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5520        // on the next token: `(` = composite, ident `ENUM` = enum.
5521        if matches!(self.peek(), Token::LParen) {
5522            self.advance();
5523            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5524            let mut field_user_types: Vec<Option<String>> = Vec::new();
5525            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5526            // is legal PG (an attribute-less composite; measured — the old
5527            // e2e note claimed PG requires at least one attribute).
5528            if matches!(self.peek(), Token::RParen) {
5529                self.advance();
5530                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5531                    name,
5532                    kind: crate::ast::TypeKind::Composite {
5533                        fields,
5534                        field_user_types,
5535                    },
5536                }));
5537            }
5538            loop {
5539                let field_name = self.expect_ident_like()?;
5540                // v7.39 (round 264) — keep the raw type name when it is not
5541                // a builtin: that is how a NESTED composite field records
5542                // which composite it holds.
5543                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5544                    self.parse_type_with_implied_flags()?;
5545                fields.push((field_name, field_type));
5546                field_user_types.push(field_user_ref);
5547                if matches!(self.peek(), Token::Comma) {
5548                    self.advance();
5549                    continue;
5550                }
5551                if matches!(self.peek(), Token::RParen) {
5552                    self.advance();
5553                    break;
5554                }
5555                return Err(self.err(alloc::format!(
5556                    "expected , or ) in composite field list, got {:?}",
5557                    self.peek()
5558                )));
5559            }
5560            if fields.is_empty() {
5561                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5562            }
5563            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5564                name,
5565                kind: crate::ast::TypeKind::Composite {
5566                    fields,
5567                    field_user_types,
5568                },
5569            }));
5570        }
5571        // Required `ENUM` ident.
5572        let kind_ident = match self.peek().clone() {
5573            Token::Ident(s) | Token::QuotedIdent(s) => s,
5574            other => {
5575                return Err(self.err(alloc::format!(
5576                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5577                )));
5578            }
5579        };
5580        if !kind_ident.eq_ignore_ascii_case("enum") {
5581            return Err(self.err(alloc::format!(
5582                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5583            )));
5584        }
5585        self.advance();
5586        if !matches!(self.peek(), Token::LParen) {
5587            return Err(self.err(alloc::format!(
5588                "expected '(' after ENUM, got {:?}",
5589                self.peek()
5590            )));
5591        }
5592        self.advance();
5593        let mut labels: Vec<String> = Vec::new();
5594        loop {
5595            match self.peek().clone() {
5596                Token::String(s) => {
5597                    self.advance();
5598                    labels.push(s);
5599                }
5600                other => {
5601                    return Err(
5602                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5603                    );
5604                }
5605            }
5606            if matches!(self.peek(), Token::Comma) {
5607                self.advance();
5608                continue;
5609            }
5610            if matches!(self.peek(), Token::RParen) {
5611                self.advance();
5612                break;
5613            }
5614            return Err(self.err(alloc::format!(
5615                "expected , or ) in ENUM label list, got {:?}",
5616                self.peek()
5617            )));
5618        }
5619        if labels.is_empty() {
5620            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5621        }
5622        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5623            name,
5624            kind: crate::ast::TypeKind::Enum { labels },
5625        }))
5626    }
5627
5628    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5629    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5630    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5631    /// consumed.
5632    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5633        let if_not_exists = self.parse_if_not_exists();
5634        let name = self.expect_ident_like()?;
5635        let mut columns: Vec<String> = Vec::new();
5636        if matches!(self.peek(), Token::LParen) {
5637            self.advance();
5638            loop {
5639                let c = self.expect_ident_like()?;
5640                columns.push(c);
5641                if matches!(self.peek(), Token::Comma) {
5642                    self.advance();
5643                    continue;
5644                }
5645                if matches!(self.peek(), Token::RParen) {
5646                    self.advance();
5647                    break;
5648                }
5649                return Err(self.err(alloc::format!(
5650                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5651                    self.peek()
5652                )));
5653            }
5654        }
5655        if !matches!(self.peek(), Token::As) {
5656            return Err(self.err(alloc::format!(
5657                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5658                self.peek()
5659            )));
5660        }
5661        self.advance();
5662        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5663        // CTEs only; the engine rejects data-modifying ones with PG's
5664        // message). A trailing `WITH [NO] DATA` can't START the body,
5665        // so WITH here heads the query.
5666        let body = if self.peek_is_with_kw() {
5667            self.advance();
5668            self.parse_nested_with_select()?
5669        } else {
5670            let body_stmt = self.parse_select_stmt()?;
5671            let Statement::Select(body) = body_stmt else {
5672                return Err(self.err(alloc::format!(
5673                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5674                )));
5675            };
5676            body
5677        };
5678        // Optional trailing `WITH [NO] DATA`.
5679        let with_data = self.parse_optional_with_data(true)?;
5680        Ok(Statement::CreateMaterializedView(
5681            crate::ast::CreateMaterializedViewStatement {
5682                temporary: false,
5683                name,
5684                if_not_exists,
5685                columns,
5686                body,
5687                with_data,
5688                as_plain_table: false,
5689            },
5690        ))
5691    }
5692
5693    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5694    /// `default_when_absent` is what to return if the tail is
5695    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5696    /// WITH DATA).
5697    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5698        let save = self.pos;
5699        // `WITH` is an Ident (not reserved in the lexer).
5700        let is_with = match self.peek() {
5701            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5702            _ => false,
5703        };
5704        if !is_with {
5705            return Ok(default_when_absent);
5706        }
5707        self.advance();
5708        // Optional `NO`.
5709        let mut with_data = true;
5710        let is_no = match self.peek() {
5711            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5712            _ => false,
5713        };
5714        if is_no {
5715            self.advance();
5716            with_data = false;
5717        }
5718        // Required `DATA` ident.
5719        let is_data = match self.peek() {
5720            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5721            _ => false,
5722        };
5723        if is_data {
5724            self.advance();
5725            Ok(with_data)
5726        } else {
5727            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5728            // parser can interpret it.
5729            self.pos = save;
5730            Ok(default_when_absent)
5731        }
5732    }
5733
5734    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5735    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5736    /// All keyword prefixes have already been consumed; the flags
5737    /// say which were present.
5738    fn parse_create_view_after_keyword(
5739        &mut self,
5740        or_replace: bool,
5741        _materialized_unused: bool,
5742        temporary: bool,
5743    ) -> Result<Statement, ParseError> {
5744        let if_not_exists = self.parse_if_not_exists();
5745        let name = self.expect_ident_like()?;
5746        // Optional `(col, col, …)` rename list.
5747        let mut columns: Vec<String> = Vec::new();
5748        if matches!(self.peek(), Token::LParen) {
5749            self.advance();
5750            loop {
5751                let c = self.expect_ident_like()?;
5752                columns.push(c);
5753                if matches!(self.peek(), Token::Comma) {
5754                    self.advance();
5755                    continue;
5756                }
5757                if matches!(self.peek(), Token::RParen) {
5758                    self.advance();
5759                    break;
5760                }
5761                return Err(self.err(alloc::format!(
5762                    "expected , or ) in VIEW column list, got {:?}",
5763                    self.peek()
5764                )));
5765            }
5766        }
5767        // Required `AS`.
5768        if !matches!(self.peek(), Token::As) {
5769            return Err(self.err(alloc::format!(
5770                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5771                self.peek()
5772            )));
5773        }
5774        self.advance();
5775        // Body: a regular SELECT statement. v7.39 (round 151) — a
5776        // WITH-headed body is legal too (read-only CTEs only; the
5777        // engine rejects data-modifying ones with PG's message).
5778        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5779        // with the check-option clause, so WITH here heads the query.
5780        let body = if self.peek_is_with_kw() {
5781            self.advance();
5782            self.parse_nested_with_select()?
5783        } else {
5784            let body_stmt = self.parse_select_stmt()?;
5785            let Statement::Select(body) = body_stmt else {
5786                return Err(self.err(alloc::format!(
5787                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5788                )));
5789            };
5790            body
5791        };
5792        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5793        // The SELECT parser stops before a trailing WITH, so it lands here.
5794        let check_option = if matches!(self.peek(),
5795            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5796        {
5797            self.advance(); // WITH
5798            let opt = match self.peek() {
5799                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5800                    self.advance();
5801                    crate::ast::ViewCheckOption::Local
5802                }
5803                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5804                    self.advance();
5805                    crate::ast::ViewCheckOption::Cascaded
5806                }
5807                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5808                _ => crate::ast::ViewCheckOption::Cascaded,
5809            };
5810            if !matches!(self.peek(),
5811                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5812            {
5813                return Err(self.err(alloc::format!(
5814                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5815                    self.peek()
5816                )));
5817            }
5818            self.advance(); // CHECK
5819            if !matches!(self.peek(),
5820                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5821            {
5822                return Err(self.err(alloc::format!(
5823                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5824                    self.peek()
5825                )));
5826            }
5827            self.advance(); // OPTION
5828            Some(opt)
5829        } else {
5830            None
5831        };
5832        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5833            name,
5834            or_replace,
5835            if_not_exists,
5836            temporary,
5837            columns,
5838            body,
5839            check_option,
5840        }))
5841    }
5842
5843    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5844    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5845    /// consumed; `temporary` carries whether TEMPORARY was seen.
5846    fn parse_create_sequence_after_keyword(
5847        &mut self,
5848        temporary: bool,
5849    ) -> Result<Statement, ParseError> {
5850        let if_not_exists = self.parse_if_not_exists();
5851        let name = self.expect_ident_like()?;
5852        // Optional `AS data_type`.
5853        let data_type = if matches!(self.peek(), Token::As) {
5854            self.advance();
5855            Some(self.parse_sequence_data_type()?)
5856        } else {
5857            None
5858        };
5859        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5860        Ok(Statement::CreateSequence(
5861            crate::ast::CreateSequenceStatement {
5862                name,
5863                if_not_exists,
5864                temporary,
5865                data_type,
5866                options,
5867            },
5868        ))
5869    }
5870
5871    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5872    /// already been consumed; this is reached after `SEQUENCE`.
5873    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5874    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5875        use crate::ast::AlterDomainAction as A;
5876        let name = self.expect_ident_like()?;
5877        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5878        let kw = match self.peek() {
5879            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5880            Token::Drop => alloc::string::String::from("drop"),
5881            Token::Default => alloc::string::String::from("default"),
5882            other => {
5883                return Err(self.err(alloc::format!(
5884                    "expected an ALTER DOMAIN action, got {other:?}"
5885                )));
5886            }
5887        };
5888        let action = match kw.as_str() {
5889            "add" => {
5890                self.advance();
5891                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5892                {
5893                    self.advance();
5894                    Some(self.expect_ident_like()?)
5895                } else {
5896                    None
5897                };
5898                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5899                    return Err(self.err(alloc::format!(
5900                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5901                        self.peek()
5902                    )));
5903                }
5904                self.advance();
5905                if !matches!(self.peek(), Token::LParen) {
5906                    return Err(self.err("expected '(' after CHECK".into()));
5907                }
5908                self.advance();
5909                let check = self.parse_expr(0)?;
5910                if !matches!(self.peek(), Token::RParen) {
5911                    return Err(self.err("expected ')' after CHECK expression".into()));
5912                }
5913                self.advance();
5914                A::AddConstraint { name: cname, check }
5915            }
5916            "drop" => {
5917                self.advance();
5918                match self.peek() {
5919                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5920                        self.advance();
5921                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5922                        {
5923                            self.advance();
5924                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5925                            {
5926                                return Err(self.err("expected EXISTS after IF".into()));
5927                            }
5928                            self.advance();
5929                            true
5930                        } else {
5931                            false
5932                        };
5933                        let cn = self.expect_ident_like()?;
5934                        A::DropConstraint {
5935                            name: cn,
5936                            if_exists,
5937                        }
5938                    }
5939                    Token::Default => {
5940                        self.advance();
5941                        A::DropDefault
5942                    }
5943                    Token::Not => {
5944                        self.advance();
5945                        if !matches!(self.peek(), Token::Null) {
5946                            return Err(self.err("expected NULL after NOT".into()));
5947                        }
5948                        self.advance();
5949                        A::DropNotNull
5950                    }
5951                    other => {
5952                        return Err(self.err(alloc::format!(
5953                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5954                        )));
5955                    }
5956                }
5957            }
5958            "set" => {
5959                self.advance();
5960                match self.peek() {
5961                    Token::Default => {
5962                        self.advance();
5963                        A::SetDefault(self.parse_expr(0)?)
5964                    }
5965                    Token::Not => {
5966                        self.advance();
5967                        if !matches!(self.peek(), Token::Null) {
5968                            return Err(self.err("expected NULL after NOT".into()));
5969                        }
5970                        self.advance();
5971                        A::SetNotNull
5972                    }
5973                    other => {
5974                        return Err(self.err(alloc::format!(
5975                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5976                        )));
5977                    }
5978                }
5979            }
5980            "rename" => {
5981                self.advance();
5982                if !matches!(self.peek(), Token::To) {
5983                    return Err(self.err("expected TO after RENAME".into()));
5984                }
5985                self.advance();
5986                A::RenameTo(self.expect_ident_like()?)
5987            }
5988            other => {
5989                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5990            }
5991        };
5992        Ok(Statement::AlterDomain { name, action })
5993    }
5994
5995    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5996        let if_exists = self.parse_if_exists();
5997        let name = self.expect_ident_like()?;
5998        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5999        // the option list (PG allows only one or the other).
6000        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6001            self.advance();
6002            if matches!(self.peek(), Token::To) {
6003                self.advance();
6004            } else {
6005                self.expect_keyword_ident("to")?;
6006            }
6007            let new = self.expect_ident_like()?;
6008            return Ok(Statement::AlterSequence(
6009                crate::ast::AlterSequenceStatement {
6010                    name,
6011                    if_exists,
6012                    options: crate::ast::SequenceOptions::default(),
6013                    rename_to: Some(new),
6014                },
6015            ));
6016        }
6017        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6018        Ok(Statement::AlterSequence(
6019            crate::ast::AlterSequenceStatement {
6020                name,
6021                if_exists,
6022                options,
6023                rename_to: None,
6024            },
6025        ))
6026    }
6027
6028    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6029        let kw = self.expect_ident_like()?;
6030        match kw.to_ascii_lowercase().as_str() {
6031            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6032            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6033            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6034            other => Err(self.err(alloc::format!(
6035                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6036            ))),
6037        }
6038    }
6039
6040    fn parse_sequence_options(
6041        &mut self,
6042        allow_restart: bool,
6043    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6044        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6045        let mut opts = SequenceOptions::default();
6046        #[allow(clippy::while_let_loop)]
6047        loop {
6048            // Match an ident; stop at any non-ident token (sentinel,
6049            // semicolon, end of statement).
6050            let kw_lc = match self.peek() {
6051                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6052                _ => break,
6053            };
6054            match kw_lc.as_str() {
6055                "increment" => {
6056                    self.advance();
6057                    // Optional BY.
6058                    if self.peek_is_by() {
6059                        self.advance();
6060                    }
6061                    opts.increment = Some(self.expect_signed_int()?);
6062                }
6063                "minvalue" => {
6064                    self.advance();
6065                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6066                }
6067                "maxvalue" => {
6068                    self.advance();
6069                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6070                }
6071                "no" => {
6072                    self.advance();
6073                    let what = self.expect_ident_like()?;
6074                    match what.to_ascii_lowercase().as_str() {
6075                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6076                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6077                        "cycle" => opts.cycle = Some(false),
6078                        other => {
6079                            return Err(self.err(alloc::format!(
6080                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6081                            )));
6082                        }
6083                    }
6084                }
6085                "start" => {
6086                    self.advance();
6087                    // Optional WITH.
6088                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6089                        if s.eq_ignore_ascii_case("with"))
6090                    {
6091                        self.advance();
6092                    }
6093                    opts.start = Some(self.expect_signed_int()?);
6094                }
6095                "restart" if allow_restart => {
6096                    self.advance();
6097                    // Optional WITH n; bare RESTART means restart at START.
6098                    let mut with_val: Option<i64> = None;
6099                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6100                        if s.eq_ignore_ascii_case("with"))
6101                    {
6102                        self.advance();
6103                        with_val = Some(self.expect_signed_int()?);
6104                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6105                        with_val = Some(self.expect_signed_int()?);
6106                    }
6107                    opts.restart = Some(with_val);
6108                }
6109                "cache" => {
6110                    self.advance();
6111                    opts.cache = Some(self.expect_signed_int()?);
6112                }
6113                "cycle" => {
6114                    self.advance();
6115                    opts.cycle = Some(true);
6116                }
6117                "owned" => {
6118                    self.advance();
6119                    match self.peek() {
6120                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6121                            self.advance();
6122                        }
6123                        other => {
6124                            return Err(
6125                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6126                            );
6127                        }
6128                    }
6129                    // OWNED BY {NONE | tab.col}. Read just one ident
6130                    // (NOT expect_ident_like which would auto-strip
6131                    // a schema prefix and consume the `.col` we need).
6132                    let first = match self.advance() {
6133                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6134                        other => {
6135                            return Err(self.err(alloc::format!(
6136                                "expected identifier or NONE after OWNED BY, got {other:?}"
6137                            )));
6138                        }
6139                    };
6140                    if first.eq_ignore_ascii_case("none") {
6141                        opts.owned_by = Some(SequenceOwnedBy::None);
6142                    } else if matches!(self.peek(), Token::Dot) {
6143                        self.advance();
6144                        let second = match self.advance() {
6145                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6146                            other => {
6147                                return Err(self.err(alloc::format!(
6148                                    "expected column name after OWNED BY {first}., got {other:?}"
6149                                )));
6150                            }
6151                        };
6152                        // v7.17 dump-compat fix — pg_dump emits
6153                        // OWNED BY clauses as
6154                        // `schema.table.column` (three segments).
6155                        // If a third `.<ident>` follows, treat the
6156                        // first ident as schema (drop it; SPG is
6157                        // single-schema) and the middle / last
6158                        // pair as table.column. Otherwise it's
6159                        // the two-segment form table.column.
6160                        if matches!(self.peek(), Token::Dot) {
6161                            self.advance();
6162                            let third = match self.advance() {
6163                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6164                                other => {
6165                                    return Err(self.err(alloc::format!(
6166                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6167                                    )));
6168                                }
6169                            };
6170                            let _ = first; // schema prefix discarded
6171                            opts.owned_by = Some(SequenceOwnedBy::Column {
6172                                table: second,
6173                                column: third,
6174                            });
6175                        } else {
6176                            opts.owned_by = Some(SequenceOwnedBy::Column {
6177                                table: first,
6178                                column: second,
6179                            });
6180                        }
6181                    } else {
6182                        return Err(self.err(alloc::format!(
6183                            "expected table.column or NONE after OWNED BY, got {first:?}"
6184                        )));
6185                    }
6186                }
6187                _ => break,
6188            }
6189        }
6190        Ok(opts)
6191    }
6192
6193    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6194        let neg = if matches!(self.peek(), Token::Minus) {
6195            self.advance();
6196            true
6197        } else {
6198            false
6199        };
6200        match self.peek() {
6201            Token::Integer(n) => {
6202                let v = *n;
6203                self.advance();
6204                Ok(if neg { -v } else { v })
6205            }
6206            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6207        }
6208    }
6209
6210    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6211    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6212    /// clause is fully accepted and discarded — SPG always runs
6213    /// constraint checks immediately (single-writer model). The
6214    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6215    /// in either order (per the SQL spec they're independent),
6216    /// though pg_dump always emits them in the canonical
6217    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6218    /// Stops at the first token that isn't part of the clause.
6219    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6220        self.consume_deferrable_clauses_timed().map(|_| ())
6221    }
6222
6223    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6224    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6225    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6226    /// NOT DEFERRABLE and a circular-FK migration could not load.
6227    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6228        let mut deferrable = false;
6229        let mut initially_deferred = false;
6230        loop {
6231            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6232            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6233                self.advance();
6234                deferrable = true;
6235                if self.consume_optional_initially_clause()? {
6236                    initially_deferred = true;
6237                }
6238                continue;
6239            }
6240            // `NOT DEFERRABLE` — already worked pre-3.1.
6241            if matches!(self.peek(), Token::Not) {
6242                let look = self.tokens.get(self.pos + 1);
6243                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6244                    self.advance(); // NOT
6245                    self.advance(); // DEFERRABLE
6246                    deferrable = false;
6247                    initially_deferred = false;
6248                    let _ = self.consume_optional_initially_clause()?;
6249                    continue;
6250                }
6251                break;
6252            }
6253            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6254            // accepts this without a leading [NOT] DEFERRABLE
6255            // (the timing keyword alone). pg_dump occasionally
6256            // emits it on FK constraints that inherit timing.
6257            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6258                if self.consume_optional_initially_clause()? {
6259                    initially_deferred = true;
6260                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6261                    deferrable = true;
6262                }
6263                continue;
6264            }
6265            break;
6266        }
6267        Ok((deferrable, initially_deferred))
6268    }
6269
6270    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6271    /// next token is `INITIALLY`, consume it plus the required
6272    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6273    /// Returns true when the timing seen was `DEFERRED`.
6274    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6275        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6276            return Ok(false);
6277        }
6278        self.advance(); // INITIALLY
6279        match self.advance() {
6280            Token::Ident(s)
6281                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6282            {
6283                Ok(s.eq_ignore_ascii_case("deferred"))
6284            }
6285            other => Err(self.err(alloc::format!(
6286                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6287            ))),
6288        }
6289    }
6290
6291    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6292    /// in its entirety so the parser returns Empty without
6293    /// touching the runtime. The CREATE+PROCEDURE keywords are
6294    /// already consumed; this swallows everything from the
6295    /// procedure name through the matching `END`, including
6296    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6297    /// (DELIMITER `//` makes the script splitter forward the
6298    /// whole block as one statement), `@var` session-variable
6299    /// references, and the trailing terminator.
6300    ///
6301    /// Tracks nesting depth so:
6302    ///   BEGIN
6303    ///     IF cond THEN
6304    ///       BEGIN ... END;
6305    ///     END IF;
6306    ///   END
6307    /// terminates at the outer END.
6308    fn consume_mysql_routine_body(&mut self) {
6309        // Outer skeleton: name, (...), optional clauses, BEGIN
6310        // <body> END [;]. Scan for the first BEGIN — anything
6311        // before it is signature decoration we don't care about.
6312        // Once inside BEGIN, count up on BEGIN, down on END.
6313        let mut depth: i32 = 0;
6314        let mut started = false;
6315        loop {
6316            match self.peek().clone() {
6317                Token::Begin => {
6318                    self.advance();
6319                    depth += 1;
6320                    started = true;
6321                }
6322                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6323                    self.advance();
6324                    if started {
6325                        depth -= 1;
6326                        if depth <= 0 {
6327                            // Optional trailing ident (`END IF`,
6328                            // `END LOOP`, `END WHILE`, `END CASE`,
6329                            // `END label_name`) — eat the next
6330                            // ident if present so we don't
6331                            // mistake `END IF;` for the outer
6332                            // close.
6333                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6334                                // If the next token is one of the
6335                                // PL/SQL block-closer keywords,
6336                                // the END belongs to an inner
6337                                // block; bump depth back up.
6338                                let is_inner_close = matches!(
6339                                    self.peek(),
6340                                    Token::Ident(s) | Token::QuotedIdent(s)
6341                                        if matches!(
6342                                            s.to_ascii_lowercase().as_str(),
6343                                            "if" | "loop" | "while" | "case" | "repeat"
6344                                        )
6345                                );
6346                                if is_inner_close {
6347                                    self.advance();
6348                                    depth += 1;
6349                                    continue;
6350                                }
6351                            }
6352                            // Eat optional trailing `;`.
6353                            if matches!(self.peek(), Token::Semicolon) {
6354                                self.advance();
6355                            }
6356                            return;
6357                        }
6358                    }
6359                }
6360                Token::Eof => return,
6361                _ => {
6362                    self.advance();
6363                }
6364            }
6365        }
6366    }
6367
6368    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6369    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6370    ///
6371    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6372    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6373    ///   ident, or `ident @ ident-or-quoted-string` host form)
6374    /// * `SQL SECURITY {DEFINER|INVOKER}`
6375    ///
6376    /// Each clause may appear at most once but in any order.
6377    /// The hints are pure planner / permission metadata that
6378    /// SPG's view-rewrite engine handles uniformly; we accept
6379    /// and discard. Returns `Ok(())` once a non-clause token is
6380    /// peeked (the caller then checks for the `VIEW` keyword).
6381    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6382        loop {
6383            match self.peek().clone() {
6384                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6385                    self.advance(); // ALGORITHM
6386                    // Optional `=`. MySQL spec requires it but be
6387                    // generous.
6388                    if matches!(self.peek(), Token::Eq) {
6389                        self.advance();
6390                    }
6391                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6392                    // bare ident; unknown values still parse so
6393                    // future MySQL versions don't break.
6394                    if matches!(
6395                        self.peek(),
6396                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6397                    ) {
6398                        self.advance();
6399                    }
6400                }
6401                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6402                    self.advance(); // DEFINER
6403                    if matches!(self.peek(), Token::Eq) {
6404                        self.advance();
6405                    }
6406                    // User: quoted string, ident, OR ident @ host
6407                    // (host may itself be quoted or bare).
6408                    match self.peek().clone() {
6409                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6410                            self.advance();
6411                            // Optional `@host`.
6412                            if matches!(self.peek(), Token::At) {
6413                                self.advance();
6414                                if matches!(
6415                                    self.peek(),
6416                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6417                                ) {
6418                                    self.advance();
6419                                }
6420                            }
6421                        }
6422                        _ => {}
6423                    }
6424                }
6425                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6426                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6427                    // when followed by SECURITY — the dispatcher must
6428                    // not consume a bare `SQL` token (it's not a
6429                    // legal CREATE prefix on its own).
6430                    let save = self.pos;
6431                    self.advance(); // SQL
6432                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6433                        if s2.eq_ignore_ascii_case("security"))
6434                    {
6435                        self.advance(); // SECURITY
6436                        // DEFINER / INVOKER trailing ident.
6437                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6438                            self.advance();
6439                        }
6440                    } else {
6441                        // Not a SQL SECURITY clause — roll back and
6442                        // bail; the caller will error out cleanly.
6443                        self.pos = save;
6444                        return Ok(());
6445                    }
6446                }
6447                _ => return Ok(()),
6448            }
6449        }
6450    }
6451
6452    fn parse_if_not_exists(&mut self) -> bool {
6453        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6454        {
6455            let save = self.pos;
6456            self.advance();
6457            if matches!(self.peek(), Token::Not) {
6458                self.advance();
6459                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6460                {
6461                    self.advance();
6462                    return true;
6463                }
6464            }
6465            self.pos = save;
6466        }
6467        false
6468    }
6469
6470    fn parse_if_exists(&mut self) -> bool {
6471        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6472        {
6473            let save = self.pos;
6474            self.advance();
6475            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6476            {
6477                self.advance();
6478                return true;
6479            }
6480            self.pos = save;
6481        }
6482        false
6483    }
6484
6485    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6486    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6487    /// been consumed.
6488    fn parse_create_trigger_after_keyword(
6489        &mut self,
6490        or_replace: bool,
6491    ) -> Result<Statement, ParseError> {
6492        let name = self.expect_ident_like()?;
6493        let timing = {
6494            let ident = self.expect_ident_like()?;
6495            if ident.eq_ignore_ascii_case("before") {
6496                TriggerTiming::Before
6497            } else if ident.eq_ignore_ascii_case("after") {
6498                TriggerTiming::After
6499            } else if ident.eq_ignore_ascii_case("instead") {
6500                let next = self.expect_ident_like()?;
6501                if !next.eq_ignore_ascii_case("of") {
6502                    return Err(self.err(alloc::format!(
6503                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6504                    )));
6505                }
6506                TriggerTiming::InsteadOf
6507            } else {
6508                return Err(self.err(alloc::format!(
6509                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6510                )));
6511            }
6512        };
6513        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6514        // OR is a reserved keyword token (Token::Or), not an Ident.
6515        // v7.13.0 — after an UPDATE event we may optionally see
6516        // `OF col, col, …` (mailrs round-5 G7). Columns are
6517        // captured into `update_columns` once across the whole
6518        // events list; multiple `UPDATE OF` clauses are rejected.
6519        let mut events: Vec<TriggerEvent> = Vec::new();
6520        let mut update_columns: Vec<String> = Vec::new();
6521        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6522        events.push(first_ev);
6523        if !first_cols.is_empty() {
6524            update_columns = first_cols;
6525        }
6526        while matches!(self.peek(), Token::Or) {
6527            self.advance();
6528            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6529            events.push(ev);
6530            if !cols.is_empty() {
6531                if !update_columns.is_empty() {
6532                    return Err(
6533                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6534                    );
6535                }
6536                update_columns = cols;
6537            }
6538        }
6539        // ON <table>
6540        let tok = self.peek();
6541        let Token::On = tok else {
6542            return Err(self.err(alloc::format!(
6543                "expected ON after trigger events, got {tok:?}"
6544            )));
6545        };
6546        self.advance();
6547        let table = self.expect_ident_like()?;
6548        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6549        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6550        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6551        // the trigger as a plain AFTER trigger (correct for every non-deferred
6552        // use; deferral timing is not yet honoured).
6553        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6554            if s.eq_ignore_ascii_case("from"))
6555        {
6556            self.advance();
6557            let _reftable = self.expect_ident_like()?;
6558        }
6559        self.consume_optional_deferrable_clauses()?;
6560        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6561        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6562        // idents.
6563        if !matches!(self.peek(), Token::For) {
6564            return Err(self.err(alloc::format!(
6565                "expected FOR EACH ROW / STATEMENT, got {:?}",
6566                self.peek()
6567            )));
6568        }
6569        self.advance();
6570        let for_each = {
6571            let e = self.expect_ident_like()?;
6572            if !e.eq_ignore_ascii_case("each") {
6573                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6574            }
6575            let unit = self.expect_ident_like()?;
6576            if unit.eq_ignore_ascii_case("row") {
6577                TriggerForEach::Row
6578            } else if unit.eq_ignore_ascii_case("statement") {
6579                TriggerForEach::Statement
6580            } else {
6581                return Err(self.err(alloc::format!(
6582                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6583                )));
6584            }
6585        };
6586        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6587        let when_condition = if matches!(self.peek(),
6588            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6589        {
6590            self.advance();
6591            Some(self.parse_paren_expr("WHEN")?)
6592        } else {
6593            None
6594        };
6595        // EXECUTE FUNCTION/PROCEDURE name(...)
6596        let exec = self.expect_ident_like()?;
6597        if !exec.eq_ignore_ascii_case("execute") {
6598            return Err(self.err(alloc::format!(
6599                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6600            )));
6601        }
6602        let fn_or_proc = self.expect_ident_like()?;
6603        if !(fn_or_proc.eq_ignore_ascii_case("function")
6604            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6605        {
6606            return Err(self.err(alloc::format!(
6607                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6608            )));
6609        }
6610        let function = self.expect_ident_like()?;
6611        // Optional empty arg list `()`.
6612        if matches!(self.peek(), Token::LParen) {
6613            self.advance();
6614            if !matches!(self.peek(), Token::RParen) {
6615                return Err(self.err(alloc::format!(
6616                    "v7.12.4 trigger function calls take no args; got {:?}",
6617                    self.peek()
6618                )));
6619            }
6620            self.advance();
6621        }
6622        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6623            name,
6624            or_replace,
6625            timing,
6626            events,
6627            table,
6628            for_each,
6629            function,
6630            update_columns,
6631            when_condition,
6632        }))
6633    }
6634
6635    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6636    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6637    fn parse_create_rule_after_keyword(
6638        &mut self,
6639        or_replace: bool,
6640    ) -> Result<Statement, ParseError> {
6641        let name = self.expect_ident_like()?;
6642        if !matches!(self.peek(), Token::As) {
6643            return Err(self.err(alloc::format!(
6644                "expected AS in CREATE RULE, got {:?}",
6645                self.peek()
6646            )));
6647        }
6648        self.advance();
6649        if !matches!(self.peek(), Token::On) {
6650            return Err(self.err(alloc::format!(
6651                "expected ON in CREATE RULE, got {:?}",
6652                self.peek()
6653            )));
6654        }
6655        self.advance();
6656        let event = self.parse_rule_event()?;
6657        if !matches!(self.peek(), Token::To)
6658            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6659        {
6660            return Err(self.err(alloc::format!(
6661                "expected TO after rule event, got {:?}",
6662                self.peek()
6663            )));
6664        }
6665        self.advance();
6666        let table = self.expect_ident_like()?;
6667        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6668        let when_condition = if matches!(self.peek(), Token::Where) {
6669            self.advance();
6670            Some(self.parse_expr(0)?)
6671        } else {
6672            None
6673        };
6674        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6675        {
6676            return Err(self.err(alloc::format!(
6677                "expected DO in CREATE RULE, got {:?}",
6678                self.peek()
6679            )));
6680        }
6681        self.advance();
6682        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6683        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6684        {
6685            self.advance();
6686            true
6687        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6688            self.advance();
6689            false
6690        } else {
6691            false
6692        };
6693        // `NOTHING` | `( cmd; … )` | `cmd`.
6694        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6695        {
6696            self.advance();
6697            Vec::new()
6698        } else if matches!(self.peek(), Token::LParen) {
6699            self.advance();
6700            let mut cmds = Vec::new();
6701            loop {
6702                cmds.push(self.parse_one_statement()?);
6703                if matches!(self.peek(), Token::Semicolon) {
6704                    self.advance();
6705                    if matches!(self.peek(), Token::RParen) {
6706                        break;
6707                    }
6708                    continue;
6709                }
6710                break;
6711            }
6712            if !matches!(self.peek(), Token::RParen) {
6713                return Err(self.err(alloc::format!(
6714                    "expected ) closing the CREATE RULE command list, got {:?}",
6715                    self.peek()
6716                )));
6717            }
6718            self.advance();
6719            cmds
6720        } else {
6721            alloc::vec![self.parse_one_statement()?]
6722        };
6723        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6724            name,
6725            or_replace,
6726            event,
6727            table,
6728            instead,
6729            when_condition,
6730            commands,
6731        }))
6732    }
6733
6734    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6735    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6736        if matches!(self.peek(), Token::Insert) {
6737            self.advance();
6738            return Ok(alloc::string::String::from("INSERT"));
6739        }
6740        if matches!(self.peek(), Token::Select) {
6741            self.advance();
6742            return Ok(alloc::string::String::from("SELECT"));
6743        }
6744        match self.peek() {
6745            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6746                self.advance();
6747                Ok(alloc::string::String::from("UPDATE"))
6748            }
6749            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6750                self.advance();
6751                Ok(alloc::string::String::from("DELETE"))
6752            }
6753            other => Err(self.err(alloc::format!(
6754                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6755            ))),
6756        }
6757    }
6758
6759    /// v7.13.0 — parse one trigger event, then optionally consume
6760    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6761    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6762    fn parse_trigger_event_with_optional_of(
6763        &mut self,
6764    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6765        let ev = self.parse_trigger_event()?;
6766        if !matches!(ev, TriggerEvent::Update) {
6767            return Ok((ev, Vec::new()));
6768        }
6769        // `OF` is a bare ident.
6770        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6771            return Ok((ev, Vec::new()));
6772        }
6773        self.advance(); // OF
6774        let mut cols: Vec<String> = Vec::new();
6775        loop {
6776            cols.push(self.expect_ident_like()?);
6777            if matches!(self.peek(), Token::Comma) {
6778                self.advance();
6779                continue;
6780            }
6781            break;
6782        }
6783        if cols.is_empty() {
6784            return Err(
6785                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6786            );
6787        }
6788        Ok((ev, cols))
6789    }
6790
6791    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6792    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6793    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6794    /// inside the body.
6795    /// Called by [`parse_plpgsql_body`] after the body's tokens
6796    /// have been lexed into this temporary parser.
6797    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6798        // v7.12.6 — optional DECLARE prelude.
6799        let declarations = if matches!(
6800            self.peek(),
6801            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6802        ) {
6803            self.advance();
6804            self.parse_plpgsql_declare_block()?
6805        } else {
6806            Vec::new()
6807        };
6808        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6809        // `BEGIN` transaction-start, but we can reuse the
6810        // reserved Token::Begin since the body is a separate
6811        // lex/parse context).
6812        if !matches!(self.peek(), Token::Begin) {
6813            return Err(self.err(alloc::format!(
6814                "expected BEGIN at start of plpgsql block, got {:?}",
6815                self.peek()
6816            )));
6817        }
6818        self.advance();
6819        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6820        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6821        // body's last statement and the trailing END. When present
6822        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6823        // arms terminated by END.
6824        let exception_handlers = if matches!(
6825            self.peek(),
6826            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6827        ) {
6828            self.advance();
6829            self.parse_plpgsql_exception_handlers()?
6830        } else {
6831            Vec::new()
6832        };
6833        Ok(PlPgSqlBlock {
6834            declarations,
6835            statements,
6836            exception_handlers,
6837        })
6838    }
6839
6840    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6841    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6842    fn parse_plpgsql_exception_handlers(
6843        &mut self,
6844    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6845        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6846        loop {
6847            // Stop at END — the block-level trailing END LOOP / END;
6848            // is handled by the caller.
6849            if matches!(
6850                self.peek(),
6851                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6852            ) {
6853                return Ok(out);
6854            }
6855            // WHEN <cond> [OR <cond>]* THEN <body>
6856            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6857            {
6858                return Err(self.err(alloc::format!(
6859                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
6860                    self.peek()
6861                )));
6862            }
6863            self.advance();
6864            let mut conditions: Vec<String> = Vec::new();
6865            conditions.push(self.expect_ident_like()?);
6866            while matches!(self.peek(), Token::Or) {
6867                self.advance();
6868                conditions.push(self.expect_ident_like()?);
6869            }
6870            let then_kw = self.expect_ident_like()?;
6871            if !then_kw.eq_ignore_ascii_case("then") {
6872                return Err(self.err(alloc::format!(
6873                    "expected THEN after WHEN condition list, got {then_kw:?}"
6874                )));
6875            }
6876            let body = self.parse_plpgsql_stmt_list_until_end()?;
6877            out.push(crate::ast::ExceptionHandler { conditions, body });
6878        }
6879    }
6880
6881    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6882    /// prelude. Caller has already consumed `DECLARE`. We stop
6883    /// reading entries when we hit `BEGIN`.
6884    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6885        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6886        loop {
6887            if matches!(self.peek(), Token::Begin) {
6888                return Ok(out);
6889            }
6890            let name = self.expect_ident_like()?;
6891            // v7.37.20 (20.7) — type inference: if the next token is
6892            // `:=` or `=` (no explicit type), infer from the default
6893            // expression. Otherwise the ident that follows is the
6894            // declared type.
6895            //
6896            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6897            // (PG-standard). SPG parse-accepts and treats identically
6898            // to inference — the eventual runtime value determines
6899            // the local's type, which is faithful to how SPG handles
6900            // untyped locals today (see 20.7). Full compile-time
6901            // catalog lookup queues with v7.40 PL/pgSQL epic.
6902            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6903                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6904                // downstream declaration walker to type the local by
6905                // the runtime type of the default expression.
6906                FunctionArgType::Raw("_infer_".into())
6907            } else {
6908                let ty_token = self.expect_ident_like()?;
6909                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6910                // consume optional `.<ident>` qualifier + `%<KW>`
6911                // suffix. Both qualifier and suffix map to _infer_.
6912                if matches!(self.peek(), Token::Dot) {
6913                    self.advance();
6914                    let _ = self.expect_ident_like()?;
6915                }
6916                if matches!(self.peek(), Token::Percent) {
6917                    self.advance();
6918                    // Consume the trailing TYPE / ROWTYPE ident.
6919                    let _ = self.expect_ident_like()?;
6920                    FunctionArgType::Raw("_infer_".into())
6921                } else {
6922                    match map_type_ident_to_column_type_name(&ty_token) {
6923                        Some(t) => FunctionArgType::Typed(t),
6924                        None => FunctionArgType::Raw(ty_token),
6925                    }
6926                }
6927            };
6928            let default = match self.peek() {
6929                Token::ColonEq => {
6930                    self.advance();
6931                    Some(self.parse_expr(0)?)
6932                }
6933                Token::Eq => {
6934                    // PL/pgSQL also accepts `=` for the
6935                    // DECLARE default (PG treats them the same
6936                    // in this position).
6937                    self.advance();
6938                    Some(self.parse_expr(0)?)
6939                }
6940                _ => None,
6941            };
6942            // Mandatory `;` between declarations.
6943            if !matches!(self.peek(), Token::Semicolon) {
6944                return Err(self.err(alloc::format!(
6945                    "expected ; after DECLARE entry for {name:?}, got {:?}",
6946                    self.peek()
6947                )));
6948            }
6949            self.advance();
6950            out.push(PlPgSqlDeclare { name, ty, default });
6951        }
6952    }
6953
6954    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6955    /// the terminating `END;` (or `END IF;` etc — handled by the
6956    /// per-construct sub-parsers). Used by both the outer block
6957    /// and the IF/ELSE branch bodies.
6958    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6959        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6960        loop {
6961            // Allow trailing semicolons + END.
6962            while matches!(self.peek(), Token::Semicolon) {
6963                self.advance();
6964            }
6965            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6966            if matches!(
6967                self.peek(),
6968                Token::Ident(s) | Token::QuotedIdent(s)
6969                    if s.eq_ignore_ascii_case("end")
6970                        || s.eq_ignore_ascii_case("else")
6971                        || s.eq_ignore_ascii_case("elsif")
6972                        || s.eq_ignore_ascii_case("elseif")
6973                        || s.eq_ignore_ascii_case("exception")
6974                        || s.eq_ignore_ascii_case("when")
6975            ) {
6976                return Ok(statements);
6977            }
6978            // Otherwise: one statement, then expect `;` or
6979            // a block-terminator keyword.
6980            let stmt = self.parse_plpgsql_stmt()?;
6981            statements.push(stmt);
6982            match self.peek() {
6983                Token::Semicolon => {
6984                    self.advance();
6985                }
6986                Token::Ident(s) | Token::QuotedIdent(s)
6987                    if s.eq_ignore_ascii_case("end")
6988                        || s.eq_ignore_ascii_case("else")
6989                        || s.eq_ignore_ascii_case("elsif")
6990                        || s.eq_ignore_ascii_case("elseif")
6991                        || s.eq_ignore_ascii_case("exception")
6992                        || s.eq_ignore_ascii_case("when") =>
6993                {
6994                    // Final statement of the block without `;`.
6995                }
6996                other => {
6997                    return Err(self.err(alloc::format!(
6998                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6999                    )));
7000                }
7001            }
7002        }
7003    }
7004
7005    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7006        // RETURN keyword?
7007        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7008        {
7009            self.advance();
7010            return self.parse_plpgsql_return();
7011        }
7012        // v7.12.6 — IF block.
7013        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7014        {
7015            self.advance();
7016            return self.parse_plpgsql_if();
7017        }
7018        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7019        // Detected by peeking that token pos+3 is Ident("execute").
7020        if matches!(self.peek(), Token::For)
7021            && matches!(
7022                self.tokens.get(self.pos + 1),
7023                Some(Token::Ident(_) | Token::QuotedIdent(_))
7024            )
7025            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7026            && matches!(
7027                self.tokens.get(self.pos + 3),
7028                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7029            )
7030        {
7031            self.advance(); // FOR
7032            let var = self.expect_ident_like()?;
7033            self.advance(); // IN
7034            self.advance(); // EXECUTE
7035            // Prescan for LOOP at paren depth 0 so parse_expr stops
7036            // before the LOOP keyword (same trick as the bare-SELECT
7037            // ForQuery arm).
7038            let mut depth: i32 = 0;
7039            let mut loop_pos: Option<usize> = None;
7040            let mut scan = self.pos;
7041            while scan < self.tokens.len() {
7042                match self.tokens.get(scan) {
7043                    Some(Token::LParen) => depth += 1,
7044                    Some(Token::RParen) => depth -= 1,
7045                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7046                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7047                    {
7048                        loop_pos = Some(scan);
7049                        break;
7050                    }
7051                    _ => {}
7052                }
7053                scan += 1;
7054            }
7055            let loop_pos = loop_pos.ok_or_else(|| {
7056                self.err(alloc::format!(
7057                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7058                ))
7059            })?;
7060            let saved_loop = self.tokens[loop_pos].clone();
7061            self.tokens[loop_pos] = Token::Semicolon;
7062            let expr_result = self.parse_expr(0);
7063            self.tokens[loop_pos] = saved_loop;
7064            let sql_expr = expr_result?;
7065            let loop_kw = self.expect_ident_like()?;
7066            if !loop_kw.eq_ignore_ascii_case("loop") {
7067                return Err(self.err(alloc::format!(
7068                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7069                )));
7070            }
7071            let body = self.parse_plpgsql_stmt_list_until_end()?;
7072            let end_kw = self.expect_ident_like()?;
7073            if !end_kw.eq_ignore_ascii_case("end") {
7074                return Err(self.err(alloc::format!(
7075                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7076                )));
7077            }
7078            let loop_kw2 = self.expect_ident_like()?;
7079            if !loop_kw2.eq_ignore_ascii_case("loop") {
7080                return Err(self.err(alloc::format!(
7081                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7082                )));
7083            }
7084            return Ok(PlPgSqlStmt::ForExecute {
7085                var,
7086                sql_expr,
7087                body,
7088            });
7089        }
7090        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7091        //
7092        // Two syntactic forms:
7093        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7094        //   FOR var IN (SELECT ...) LOOP ...
7095        //
7096        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7097        // the trailing `LOOP` keyword as a table alias, we prescan
7098        // forward to find LOOP at paren depth 0, splice a fake
7099        // Semicolon at that position (so SELECT parses cleanly),
7100        // then re-splice LOOP back in.
7101        //
7102        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7103        // LOOP directly — no scan required.
7104        if matches!(self.peek(), Token::For)
7105            && matches!(
7106                self.tokens.get(self.pos + 1),
7107                Some(Token::Ident(_) | Token::QuotedIdent(_))
7108            )
7109            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7110            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7111                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7112        {
7113            self.advance(); // FOR
7114            let var = self.expect_ident_like()?;
7115            // IN
7116            self.advance();
7117            let query = if matches!(self.peek(), Token::LParen) {
7118                // Paren-wrapped SELECT.
7119                self.advance();
7120                let inner = self.parse_select_stmt()?;
7121                let Statement::Select(q) = inner else {
7122                    return Err(self.err(alloc::format!(
7123                        "expected SELECT inside (…), got {:?}",
7124                        self.peek()
7125                    )));
7126                };
7127                if !matches!(self.peek(), Token::RParen) {
7128                    return Err(self.err(alloc::format!(
7129                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7130                        self.peek()
7131                    )));
7132                }
7133                self.advance();
7134                q
7135            } else {
7136                // Bare SELECT: prescan to find the LOOP boundary.
7137                let mut depth: i32 = 0;
7138                let mut loop_pos: Option<usize> = None;
7139                let mut scan = self.pos;
7140                while scan < self.tokens.len() {
7141                    match self.tokens.get(scan) {
7142                        Some(Token::LParen) => depth += 1,
7143                        Some(Token::RParen) => depth -= 1,
7144                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7145                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7146                        {
7147                            loop_pos = Some(scan);
7148                            break;
7149                        }
7150                        _ => {}
7151                    }
7152                    scan += 1;
7153                }
7154                let loop_pos = loop_pos.ok_or_else(|| {
7155                    self.err(alloc::format!(
7156                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7157                    ))
7158                })?;
7159                // Swap the LOOP token with a synthetic Semicolon so
7160                // parse_select_stmt stops there, then restore afterward.
7161                let saved_loop = self.tokens[loop_pos].clone();
7162                self.tokens[loop_pos] = Token::Semicolon;
7163                let parse_result = self.parse_select_stmt();
7164                self.tokens[loop_pos] = saved_loop;
7165                let inner = parse_result?;
7166                let Statement::Select(q) = inner else {
7167                    return Err(self.err(alloc::format!(
7168                        "expected SELECT after FOR <var> IN, got {:?}",
7169                        self.peek()
7170                    )));
7171                };
7172                q
7173            };
7174            let loop_kw = self.expect_ident_like()?;
7175            if !loop_kw.eq_ignore_ascii_case("loop") {
7176                return Err(self.err(alloc::format!(
7177                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7178                )));
7179            }
7180            let body = self.parse_plpgsql_stmt_list_until_end()?;
7181            let end_kw = self.expect_ident_like()?;
7182            if !end_kw.eq_ignore_ascii_case("end") {
7183                return Err(self.err(alloc::format!(
7184                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7185                )));
7186            }
7187            let loop_kw2 = self.expect_ident_like()?;
7188            if !loop_kw2.eq_ignore_ascii_case("loop") {
7189                return Err(self.err(alloc::format!(
7190                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7191                )));
7192            }
7193            return Ok(PlPgSqlStmt::ForQuery {
7194                var,
7195                query: Box::new(query),
7196                body,
7197            });
7198        }
7199        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7200        // FOR is a reserved keyword token (Token::For).
7201        if matches!(self.peek(), Token::For)
7202            && matches!(
7203                self.tokens.get(self.pos + 1),
7204                Some(Token::Ident(_) | Token::QuotedIdent(_))
7205            )
7206            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7207        {
7208            self.advance(); // FOR
7209            let var = self.expect_ident_like()?;
7210            if !matches!(self.peek(), Token::In) {
7211                return Err(self.err(alloc::format!(
7212                    "expected IN after FOR <var>, got {:?}",
7213                    self.peek()
7214                )));
7215            }
7216            self.advance();
7217            let reverse = matches!(
7218                self.peek(),
7219                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7220            );
7221            if reverse {
7222                self.advance();
7223            }
7224            let start = self.parse_expr(0)?;
7225            if !matches!(self.peek(), Token::DotDot) {
7226                return Err(self.err(alloc::format!(
7227                    "expected '..' between FOR loop bounds, got {:?}",
7228                    self.peek()
7229                )));
7230            }
7231            self.advance();
7232            let end = self.parse_expr(0)?;
7233            let loop_kw = self.expect_ident_like()?;
7234            if !loop_kw.eq_ignore_ascii_case("loop") {
7235                return Err(self.err(alloc::format!(
7236                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7237                )));
7238            }
7239            let body = self.parse_plpgsql_stmt_list_until_end()?;
7240            let end_kw = self.expect_ident_like()?;
7241            if !end_kw.eq_ignore_ascii_case("end") {
7242                return Err(self.err(alloc::format!(
7243                    "expected END LOOP after FOR body, got {end_kw:?}"
7244                )));
7245            }
7246            let loop_kw2 = self.expect_ident_like()?;
7247            if !loop_kw2.eq_ignore_ascii_case("loop") {
7248                return Err(self.err(alloc::format!(
7249                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7250                )));
7251            }
7252            return Ok(PlPgSqlStmt::ForRange {
7253                var,
7254                start,
7255                end,
7256                reverse,
7257                body,
7258            });
7259        }
7260        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7261        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7262        {
7263            self.advance();
7264            let body = self.parse_plpgsql_stmt_list_until_end()?;
7265            let end_kw = self.expect_ident_like()?;
7266            if !end_kw.eq_ignore_ascii_case("end") {
7267                return Err(self.err(alloc::format!(
7268                    "expected END LOOP after LOOP body, got {end_kw:?}"
7269                )));
7270            }
7271            let loop_kw = self.expect_ident_like()?;
7272            if !loop_kw.eq_ignore_ascii_case("loop") {
7273                return Err(self.err(alloc::format!(
7274                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7275                )));
7276            }
7277            return Ok(PlPgSqlStmt::Loop { body });
7278        }
7279        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7280        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7281        {
7282            self.advance();
7283            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7284            {
7285                self.advance();
7286                Some(self.parse_expr(0)?)
7287            } else {
7288                None
7289            };
7290            return Ok(PlPgSqlStmt::Exit { when });
7291        }
7292        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7293        // already-parsed Statement or a runtime-computed SQL string.
7294        // The disambiguator vs the extended-query-protocol `EXECUTE
7295        // <stmt_name>` (which is a top-level Statement, not a
7296        // plpgsql line) is that inside a DO block / trigger body the
7297        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7298        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7299        {
7300            self.advance();
7301            let sql = self.parse_expr(0)?;
7302            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7303        }
7304        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7305        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7306        {
7307            self.advance();
7308            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7309            {
7310                self.advance();
7311                Some(self.parse_expr(0)?)
7312            } else {
7313                None
7314            };
7315            return Ok(PlPgSqlStmt::Continue { when });
7316        }
7317        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7318        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7319        {
7320            self.advance();
7321            let condition = self.parse_expr(0)?;
7322            let loop_kw = self.expect_ident_like()?;
7323            if !loop_kw.eq_ignore_ascii_case("loop") {
7324                return Err(self.err(alloc::format!(
7325                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7326                )));
7327            }
7328            let body = self.parse_plpgsql_stmt_list_until_end()?;
7329            // Expect END LOOP.
7330            let end_kw = self.expect_ident_like()?;
7331            if !end_kw.eq_ignore_ascii_case("end") {
7332                return Err(self.err(alloc::format!(
7333                    "expected END LOOP after WHILE body, got {end_kw:?}"
7334                )));
7335            }
7336            let loop_kw2 = self.expect_ident_like()?;
7337            if !loop_kw2.eq_ignore_ascii_case("loop") {
7338                return Err(self.err(alloc::format!(
7339                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7340                )));
7341            }
7342            return Ok(PlPgSqlStmt::While { condition, body });
7343        }
7344        // v7.12.6 — RAISE.
7345        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7346        {
7347            self.advance();
7348            return self.parse_plpgsql_raise();
7349        }
7350        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7351        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7352        {
7353            self.advance();
7354            let condition = self.parse_expr(0)?;
7355            let message = if matches!(self.peek(), Token::Comma) {
7356                self.advance();
7357                Some(self.parse_expr(0)?)
7358            } else {
7359                None
7360            };
7361            return Ok(PlPgSqlStmt::Assert { condition, message });
7362        }
7363        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7364        //   "PERFORM is equivalent to SELECT but discards the
7365        //    result." Side effects (function calls, RAISE inside
7366        //    SQL functions, etc.) still execute. We desugar to
7367        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7368        //    existing embedded-statement path handles execution +
7369        //    result-discard cleanly. The result is naturally
7370        //    discarded because EmbeddedSql doesn't propagate row
7371        //    sets back to the plpgsql interpreter.
7372        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7373        {
7374            self.advance();
7375            // Splice a synthetic Token::Select into the stream at
7376            // the current position so parse_select_stmt parses the
7377            // remainder as a normal SELECT body. Token-stream
7378            // surgery mirrors the try_parse_plpgsql_select_into
7379            // pattern used for SELECT … INTO desugaring.
7380            self.tokens.insert(self.pos, Token::Select);
7381            let select = self.parse_select_stmt()?;
7382            let Statement::Select(s) = select else {
7383                return Err(self.err(alloc::format!(
7384                    "expected SELECT body after PERFORM, got {:?}",
7385                    self.peek()
7386                )));
7387            };
7388            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7389        }
7390        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7391        // plpgsql-specific shape (mailrs round-10 migrate-042).
7392        // PG's SELECT INTO at top-level SQL would CREATE a new
7393        // table; inside plpgsql it ASSIGNS the query result to
7394        // a local variable. We detect the INTO at paren-depth
7395        // 0 between SELECT and the statement boundary; if
7396        // found, split the token stream into "pre-INTO
7397        // projection" + "var" + "post-INTO FROM/WHERE…" and
7398        // rebuild as a SelectInto with a regular SELECT body
7399        // (no INTO clause).
7400        if matches!(self.peek(), Token::Select)
7401            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7402        {
7403            return Ok(PlPgSqlStmt::SelectInto {
7404                var: var_name,
7405                body: Box::new(select_body),
7406            });
7407        }
7408        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7409        // SELECT can appear directly inside a trigger body; we
7410        // recurse into the regular Statement parser, which will
7411        // stop at the trailing `;` (which our caller then
7412        // consumes).
7413        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7414        // also embed ALTER / CREATE / DROP statements; route
7415        // those through the same parser so the DO body parses
7416        // cleanly.
7417        if matches!(self.peek(), Token::Insert)
7418            || matches!(self.peek(), Token::Select)
7419            || matches!(self.peek(), Token::Create)
7420            || matches!(self.peek(), Token::Drop)
7421            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7422                if s.eq_ignore_ascii_case("update")
7423                    || s.eq_ignore_ascii_case("delete")
7424                    || s.eq_ignore_ascii_case("alter"))
7425        {
7426            let stmt = self.parse_one_statement()?;
7427            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7428        }
7429        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7430        // followed by `:=` and an expression.
7431        let target = self.parse_plpgsql_assign_target()?;
7432        // PL/pgSQL assignment uses `:=`. The lexer represents
7433        // this as a colon followed by `=`; check both shapes.
7434        match self.peek() {
7435            Token::ColonEq => {
7436                self.advance();
7437            }
7438            Token::Colon => {
7439                self.advance();
7440                if !matches!(self.peek(), Token::Eq) {
7441                    return Err(self.err(alloc::format!(
7442                        "expected := after plpgsql assign target, got `:` then {:?}",
7443                        self.peek()
7444                    )));
7445                }
7446                self.advance();
7447            }
7448            other => {
7449                return Err(self.err(alloc::format!(
7450                    "expected := after plpgsql assign target, got {other:?}"
7451                )));
7452            }
7453        }
7454        let value = self.parse_expr(0)?;
7455        Ok(PlPgSqlStmt::Assign { target, value })
7456    }
7457
7458    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7459    /// [ELSE body] END IF`. `IF` keyword already consumed.
7460    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7461        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7462        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7463        loop {
7464            // <expr> THEN
7465            let cond = self.parse_expr(0)?;
7466            let then_kw = self.expect_ident_like()?;
7467            if !then_kw.eq_ignore_ascii_case("then") {
7468                return Err(self.err(alloc::format!(
7469                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7470                )));
7471            }
7472            let body = self.parse_plpgsql_stmt_list_until_end()?;
7473            branches.push((cond, body));
7474            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7475            match self.peek() {
7476                Token::Ident(s) | Token::QuotedIdent(s)
7477                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7478                {
7479                    self.advance();
7480                    continue;
7481                }
7482                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7483                    self.advance();
7484                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7485                    break;
7486                }
7487                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7488                    break;
7489                }
7490                other => {
7491                    return Err(self.err(alloc::format!(
7492                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7493                    )));
7494                }
7495            }
7496        }
7497        // Expect `END IF` (the END keyword is the one we're
7498        // looking at right now).
7499        let end_kw = self.expect_ident_like()?;
7500        if !end_kw.eq_ignore_ascii_case("end") {
7501            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7502        }
7503        let if_kw = self.expect_ident_like()?;
7504        if !if_kw.eq_ignore_ascii_case("if") {
7505            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7506        }
7507        Ok(PlPgSqlStmt::If {
7508            branches,
7509            else_branch,
7510        })
7511    }
7512
7513    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7514    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7515    /// is already consumed.
7516    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7517        let lvl_ident = self.expect_ident_like()?;
7518        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7519            "notice" => RaiseLevel::Notice,
7520            "warning" => RaiseLevel::Warning,
7521            "info" => RaiseLevel::Info,
7522            "log" => RaiseLevel::Log,
7523            "debug" => RaiseLevel::Debug,
7524            "exception" => RaiseLevel::Exception,
7525            other => {
7526                return Err(self.err(alloc::format!(
7527                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7528                )));
7529            }
7530        };
7531        // Message: required for v7.12.6. PG accepts a bare
7532        // RAISE-rethrow form (no message), reserved for future
7533        // RAISE-no-args support.
7534        let Token::String(msg) = self.peek() else {
7535            return Err(self.err(alloc::format!(
7536                "expected RAISE message string, got {:?}",
7537                self.peek()
7538            )));
7539        };
7540        let message = msg.clone();
7541        self.advance();
7542        // Optional comma-separated args (PG `%` format substitution).
7543        let mut args: Vec<Expr> = Vec::new();
7544        while matches!(self.peek(), Token::Comma) {
7545            self.advance();
7546            args.push(self.parse_expr(0)?);
7547        }
7548        Ok(PlPgSqlStmt::Raise {
7549            level,
7550            message,
7551            args,
7552        })
7553    }
7554
7555    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7556    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7557    /// migrate-042). Returns `(rebuilt_select_without_into,
7558    /// var_name)` when the pattern matches; `None` for
7559    /// regular SELECTs (those go through the embedded-SQL
7560    /// path). Token-stream surgery so the rebuilt SELECT
7561    /// parses through the regular `parse_select_stmt`.
7562    #[allow(clippy::too_many_lines)]
7563    fn try_parse_plpgsql_select_into(
7564        &mut self,
7565    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7566        // Scan forward from `self.pos + 1` (past Token::Select)
7567        // for Token::Into at paren-depth 0, stopping at the
7568        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7569        // end the plpgsql statement.
7570        let start = self.pos;
7571        let mut into_pos: Option<usize> = None;
7572        let mut depth: i32 = 0;
7573        let mut i = start + 1;
7574        while i < self.tokens.len() {
7575            match &self.tokens[i] {
7576                Token::LParen => depth += 1,
7577                Token::RParen => depth -= 1,
7578                Token::Semicolon if depth == 0 => break,
7579                Token::Ident(s)
7580                    if depth == 0
7581                        && (s.eq_ignore_ascii_case("end")
7582                            || s.eq_ignore_ascii_case("else")
7583                            || s.eq_ignore_ascii_case("elsif")) =>
7584                {
7585                    break;
7586                }
7587                Token::Into if depth == 0 => {
7588                    into_pos = Some(i);
7589                    break;
7590                }
7591                _ => {}
7592            }
7593            i += 1;
7594        }
7595        let Some(into_at) = into_pos else {
7596            return Ok(None);
7597        };
7598        // The token immediately after INTO must be the target
7599        // var ident; anything else (e.g. INSERT INTO table)
7600        // ruled out by the depth-0 check above. Capture it.
7601        let var = match self.tokens.get(into_at + 1) {
7602            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7603            other => {
7604                return Err(self.err(alloc::format!(
7605                    "expected variable name after SELECT … INTO, got {other:?}"
7606                )));
7607            }
7608        };
7609        // Find the end of the plpgsql SELECT INTO statement —
7610        // same boundary rules as the depth-0 scan above.
7611        let mut end = into_at + 2;
7612        let mut depth2: i32 = 0;
7613        while end < self.tokens.len() {
7614            match &self.tokens[end] {
7615                Token::LParen => depth2 += 1,
7616                Token::RParen => depth2 -= 1,
7617                Token::Semicolon if depth2 == 0 => break,
7618                Token::Ident(s)
7619                    if depth2 == 0
7620                        && (s.eq_ignore_ascii_case("end")
7621                            || s.eq_ignore_ascii_case("else")
7622                            || s.eq_ignore_ascii_case("elsif")) =>
7623                {
7624                    break;
7625                }
7626                _ => {}
7627            }
7628            end += 1;
7629        }
7630        // Rebuild a token stream that represents the SELECT
7631        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7632        // post-var tokens up to statement end]. Run the
7633        // regular `parse_select_stmt` against it.
7634        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7635        for j in start..into_at {
7636            rebuilt.push(self.tokens[j].clone());
7637        }
7638        for j in (into_at + 2)..end {
7639            rebuilt.push(self.tokens[j].clone());
7640        }
7641        rebuilt.push(Token::Eof);
7642        let saved_pos = self.pos;
7643        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7644        self.pos = 0;
7645        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7646        if !matches!(self.peek(), Token::Select) {
7647            self.tokens = saved_tokens;
7648            self.pos = saved_pos;
7649            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7650        }
7651        let sel = self.parse_select_stmt();
7652        self.tokens = saved_tokens;
7653        self.pos = end;
7654        let sel = sel?;
7655        let Statement::Select(body) = sel else {
7656            return Err(self.err(alloc::format!(
7657                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7658            )));
7659        };
7660        Ok(Some((body, var)))
7661    }
7662
7663    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7664        // v7.16.1 — read the head token DIRECTLY rather than
7665        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7666        // strip (`public.t` → `t`) inside `expect_ident_like`
7667        // greedily consumes any `ident . ident` pair, which
7668        // silently turned every `NEW.col := …` /
7669        // `OLD.col := …` plpgsql assignment into a Local("col")
7670        // assignment — the head "new"/"old" was eaten as if it
7671        // were a schema name and the Dot was consumed too, so
7672        // this function's own `peek() == Token::Dot` check
7673        // below never fired. Every BEFORE trigger that rewrote
7674        // a NEW cell was a silent no-op for two major releases
7675        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7676        // gate failures were investigated as v7.16.1 backlog.
7677        let head = match self.advance() {
7678            Token::Ident(s) | Token::QuotedIdent(s) => s,
7679            other => {
7680                return Err(self.err(alloc::format!(
7681                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7682                )));
7683            }
7684        };
7685        if matches!(self.peek(), Token::Dot) {
7686            self.advance();
7687            let col = self.expect_ident_like()?;
7688            if head.eq_ignore_ascii_case("new") {
7689                return Ok(AssignTarget::NewColumn(col));
7690            }
7691            if head.eq_ignore_ascii_case("old") {
7692                return Ok(AssignTarget::OldColumn(col));
7693            }
7694            return Err(self.err(alloc::format!(
7695                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7696                 got {head:?}.<col>"
7697            )));
7698        }
7699        Ok(AssignTarget::Local(head))
7700    }
7701
7702    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7703        // RETURN NEW / OLD / NULL — bare-ident forms.
7704        match self.peek() {
7705            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7706                self.advance();
7707                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7708            }
7709            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7710                self.advance();
7711                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7712            }
7713            Token::Null => {
7714                self.advance();
7715                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7716            }
7717            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7718            // per PL/pgSQL convention.
7719            Token::Semicolon => {
7720                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7721            }
7722            _ => {}
7723        }
7724        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7725        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7726        // caller-visible effect (blocks don't return sets), so we
7727        // desugar it identically to PERFORM: parse the SELECT (or
7728        // EXECUTE dynamic) as embedded SQL that runs for side
7729        // effects and discards the result. RETURN NEXT <expr>
7730        // (single-row accumulator) queues with v7.40 SETOF function
7731        // infrastructure.
7732        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7733        // and keep going.
7734        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7735        {
7736            self.advance();
7737            let e = self.parse_expr(0)?;
7738            return Ok(PlPgSqlStmt::ReturnNext(e));
7739        }
7740        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7741        {
7742            self.advance();
7743            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7744            // rows go to the set, like the static form. It used to desugar to a
7745            // bare ExecuteDynamic, whose result was DISCARDED.
7746            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7747            {
7748                self.advance();
7749                let sql = self.parse_expr(0)?;
7750                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7751            }
7752            // Bare RETURN QUERY <select>. If the current token is
7753            // not already SELECT (e.g., the user wrote `RETURN QUERY
7754            // <projection> FROM ...` in a shorthand — rare but PG
7755            // accepts a bare projection here), splice one in. Same
7756            // trick as PERFORM.
7757            if !matches!(self.peek(), Token::Select) {
7758                self.tokens.insert(self.pos, Token::Select);
7759            }
7760            let select = self.parse_select_stmt()?;
7761            let Statement::Select(s) = select else {
7762                return Err(self.err(alloc::format!(
7763                    "expected SELECT body after RETURN QUERY, got {:?}",
7764                    self.peek()
7765                )));
7766            };
7767            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7768            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7769            // in a SETOF function is the entire answer thrown away.
7770            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7771        }
7772        // Fall through: parse a full expression.
7773        let e = self.parse_expr(0)?;
7774        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7775    }
7776
7777    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7778        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7779        // are ident-shaped (the parser keys off case-insensitive
7780        // match — same shape used by the top-level Update / Delete
7781        // dispatchers at parse_one_statement).
7782        if matches!(self.peek(), Token::Insert) {
7783            self.advance();
7784            return Ok(TriggerEvent::Insert);
7785        }
7786        match self.peek() {
7787            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7788                self.advance();
7789                Ok(TriggerEvent::Update)
7790            }
7791            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7792                self.advance();
7793                Ok(TriggerEvent::Delete)
7794            }
7795            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7796                self.advance();
7797                Ok(TriggerEvent::Truncate)
7798            }
7799            other => Err(self.err(alloc::format!(
7800                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7801            ))),
7802        }
7803    }
7804
7805    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7806    ///   - (no clause) → implicit `FOR ALL TABLES`
7807    ///   - `FOR ALL TABLES`
7808    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7809    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7810    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7811    ///     REJECTS the bare plural (`invalid publication object list`,
7812    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7813    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7814    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7815        let name = self.expect_ident_or_string()?;
7816        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7817        // shape so existing publications keep parsing identically.
7818        let scope = if matches!(self.peek(), Token::For) {
7819            self.advance();
7820            if matches!(self.peek(), Token::All) {
7821                self.advance();
7822                if !matches!(self.peek(), Token::Tables) {
7823                    return Err(self.err(format!(
7824                        "expected TABLES after FOR ALL, got {:?}",
7825                        self.peek()
7826                    )));
7827                }
7828                self.advance();
7829                if matches!(self.peek(), Token::Except) {
7830                    self.advance();
7831                    let tables = self.parse_publication_table_list()?;
7832                    PublicationScope::AllTablesExcept(tables)
7833                } else {
7834                    PublicationScope::AllTables
7835                }
7836            } else if matches!(self.peek(), Token::Table) {
7837                self.advance();
7838                let tables = self.parse_publication_table_list()?;
7839                PublicationScope::ForTables(tables)
7840            } else if matches!(self.peek(), Token::Tables) {
7841                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7842                // plural (`FOR TABLES t`) is REJECTED (`invalid
7843                // publication object list`); TABLES only pairs with
7844                // `IN SCHEMA`. The old arm accepted it on an
7845                // unverifiable "PG 19 accepts both" claim.
7846                self.advance();
7847                if !matches!(self.peek(), Token::In) {
7848                    return Err(self.err(alloc::string::String::from(
7849                        "invalid publication object list",
7850                    )));
7851                }
7852                self.advance();
7853                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7854                    return Err(self.err(format!(
7855                        "expected SCHEMA after FOR TABLES IN, got {:?}",
7856                        self.peek()
7857                    )));
7858                }
7859                self.advance();
7860                let schema = self.expect_ident_or_string()?;
7861                PublicationScope::TablesInSchema(schema)
7862            } else {
7863                return Err(self.err(format!(
7864                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7865                    self.peek()
7866                )));
7867            }
7868        } else {
7869            PublicationScope::AllTables
7870        };
7871        Ok(Statement::CreatePublication(CreatePublicationStatement {
7872            name,
7873            scope,
7874        }))
7875    }
7876
7877    /// v6.1.3 — Comma-separated identifier list for the publication
7878    /// FOR-clause. Requires at least one entry; empty list is a
7879    /// parse error (PG behaviour). Quoted idents are accepted; the
7880    /// names round-trip through `Display` as `quote_ident(name)`.
7881    ///
7882    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7883    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7884    /// pg_dump output. SPG's publication state today is per-table
7885    /// only (matching the pre-PG-15 surface); the col list + WHERE
7886    /// are parsed so dumps load through and the table name reaches
7887    /// `PublicationScope::ForTables`, but the filter is not enforced
7888    /// at publish time. Re-open when a customer dogfood gate
7889    /// requires per-row-filter or column-subset publish semantics
7890    /// (which gates on persistent slot state landing first, 21.12).
7891    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7892        let first = self.parse_publication_table_entry()?;
7893        let mut out = alloc::vec![first];
7894        while matches!(self.peek(), Token::Comma) {
7895            self.advance();
7896            out.push(self.parse_publication_table_entry()?);
7897        }
7898        Ok(out)
7899    }
7900
7901    /// One table entry inside a FOR TABLE clause:
7902    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7903    /// Returns just the table name; the column list + WHERE predicate
7904    /// are consumed and discarded per the parse-accept-discard
7905    /// commitment above.
7906    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7907        let name = self.expect_ident_like()?;
7908        // Optional column list — `(col, col, …)`.
7909        if matches!(self.peek(), Token::LParen) {
7910            self.advance();
7911            // Empty parens are a PG error too; require ≥ 1 column.
7912            let _ = self.expect_ident_like()?;
7913            while matches!(self.peek(), Token::Comma) {
7914                self.advance();
7915                let _ = self.expect_ident_like()?;
7916            }
7917            if !matches!(self.peek(), Token::RParen) {
7918                return Err(self.err(alloc::format!(
7919                    "expected ')' to close publication column list, got {:?}",
7920                    self.peek()
7921                )));
7922            }
7923            self.advance();
7924        }
7925        // Optional row filter — `WHERE (predicate)`.
7926        if matches!(self.peek(), Token::Where) {
7927            self.advance();
7928            if !matches!(self.peek(), Token::LParen) {
7929                return Err(self.err(alloc::format!(
7930                    "expected '(' after WHERE in publication row filter, got {:?}",
7931                    self.peek()
7932                )));
7933            }
7934            self.advance();
7935            let _ = self.parse_expr(0)?;
7936            if !matches!(self.peek(), Token::RParen) {
7937                return Err(self.err(alloc::format!(
7938                    "expected ')' to close publication WHERE filter, got {:?}",
7939                    self.peek()
7940                )));
7941            }
7942            self.advance();
7943        }
7944        Ok(name)
7945    }
7946
7947    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7948    ///                 CONNECTION '<conn>'
7949    ///                 PUBLICATION <pub> [, <pub> ...]`.
7950    ///
7951    /// The clause order is fixed (CONNECTION first, then
7952    /// PUBLICATION) to match PG. No WITH-options accepted in
7953    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7954    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7955        let name = self.expect_ident_or_string()?;
7956        if !matches!(self.peek(), Token::Connection) {
7957            return Err(self.err(format!(
7958                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7959                self.peek()
7960            )));
7961        }
7962        self.advance();
7963        let conn_str = self.expect_string_literal()?;
7964        if !matches!(self.peek(), Token::Publication) {
7965            return Err(self.err(format!(
7966                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7967                self.peek()
7968            )));
7969        }
7970        self.advance();
7971        // Reuse the publication FOR-list parser shape: at least one
7972        // identifier, comma-separated.
7973        let first = self.expect_ident_like()?;
7974        let mut publications = alloc::vec![first];
7975        while matches!(self.peek(), Token::Comma) {
7976            self.advance();
7977            publications.push(self.expect_ident_like()?);
7978        }
7979        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7980            name,
7981            conn_str,
7982            publications,
7983        }))
7984    }
7985
7986    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7987    /// All keywords after `WAIT` are bare idents in v6.1.x; no
7988    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7989    /// that fit `u64`.
7990    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7991    /// qualifier is a *namespace* the app owns (`app.user_id`,
7992    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7993    /// to discard. So parse the raw segments here instead of
7994    /// `expect_ident_like`, which strips a leading `schema.` qualifier
7995    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7996    /// a single segment and round-trip unchanged.
7997    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7998        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7999        loop {
8000            let seg = match self.advance() {
8001                Token::Ident(s) | Token::QuotedIdent(s) => s,
8002                other if unreserved_keyword_text(&other).is_some() => {
8003                    unreserved_keyword_text(&other).unwrap()
8004                }
8005                other => {
8006                    return Err(ParseError {
8007                        message: format!("expected parameter name, got {other:?}"),
8008                        token_pos: self.consumed_pos(),
8009                    });
8010                }
8011            };
8012            parts.push(seg);
8013            if matches!(self.peek(), Token::Dot) {
8014                self.advance();
8015                continue;
8016            }
8017            break;
8018        }
8019        Ok(parts.join(".").to_ascii_lowercase())
8020    }
8021
8022    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8023        Self::parse_set_value_inner(self)
8024    }
8025
8026    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8027        match self.advance() {
8028            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8029            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8030                Ok(crate::ast::SetValue::Default)
8031            }
8032            Token::Ident(s) | Token::QuotedIdent(s) => {
8033                let mut accum = s;
8034                while matches!(self.peek(), Token::Dot) {
8035                    self.advance();
8036                    let next = self.expect_ident_like()?;
8037                    accum.push('.');
8038                    accum.push_str(&next);
8039                }
8040                Ok(crate::ast::SetValue::Ident(accum))
8041            }
8042            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8043            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8044            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8045            // spellings that lex as keyword tokens, not idents:
8046            // `SET standard_conforming_strings = on` is in every
8047            // pg_dump preamble (`off` already lexes as an ident).
8048            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8049            // DEFAULT lexes as its keyword token, so the ident arm above
8050            // never saw it and the everyday reset form was a syntax error.
8051            Token::Default => Ok(crate::ast::SetValue::Default),
8052            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8053            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8054            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8055            // v7.14.0 — MySQL session/user variable RHS
8056            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8057            // Wrap as Ident so the SET handler can record it; the
8058            // engine treats `@VAR` / `@@VAR` values as opaque
8059            // strings.
8060            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8061            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8062            // is the common MySQL preamble shape. Allow a `+` or
8063            // `-` prefix on negative numerics for parity with PG
8064            // (some param defaults are negative).
8065            Token::Minus => match self.advance() {
8066                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8067                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8068                other => Err(self.err(format!(
8069                    "expected numeric after `-` in SET value, got {other:?}"
8070                ))),
8071            },
8072            other => Err(self.err(format!(
8073                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8074            ))),
8075        }
8076    }
8077
8078    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8079    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8080    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8081    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8082    /// present). Modes are comma-separated per PG; SPG also
8083    /// accepts space-separated for tolerance. READ ONLY / WRITE
8084    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8085    /// surface but not behaviorally honoured today).
8086    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8087    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8088    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8089    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8090    /// session default rather than forcing READ COMMITTED.
8091    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8092        let mut level = IsolationLevel::default();
8093        let mut have_level = false;
8094        loop {
8095            // ISOLATION LEVEL …
8096            let saw_isolation =
8097                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8098            if saw_isolation {
8099                self.advance(); // ISOLATION
8100                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8101                    return Err(self.err(alloc::format!(
8102                        "expected LEVEL after ISOLATION, got {:?}",
8103                        self.peek()
8104                    )));
8105                }
8106                self.advance(); // LEVEL
8107                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8108                let w1 = self
8109                    .expect_ident_like()
8110                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8111                let lc = w1.to_ascii_lowercase();
8112                level = match lc.as_str() {
8113                    "serializable" => IsolationLevel::Serializable,
8114                    "repeatable" => {
8115                        // Expect READ
8116                        let w2 = self
8117                            .expect_ident_like()
8118                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8119                        if !w2.eq_ignore_ascii_case("read") {
8120                            return Err(self.err(alloc::format!(
8121                                "expected READ after REPEATABLE, got {w2:?}"
8122                            )));
8123                        }
8124                        IsolationLevel::RepeatableRead
8125                    }
8126                    "read" => {
8127                        let w2 = self
8128                            .expect_ident_like()
8129                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8130                        match w2.to_ascii_lowercase().as_str() {
8131                            "committed" => IsolationLevel::ReadCommitted,
8132                            "uncommitted" => IsolationLevel::ReadUncommitted,
8133                            other => {
8134                                return Err(self.err(alloc::format!(
8135                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8136                                )));
8137                            }
8138                        }
8139                    }
8140                    other => {
8141                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8142                    }
8143                };
8144                have_level = true;
8145            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8146                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8147                self.advance();
8148                match self.peek().clone() {
8149                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8150                        self.advance();
8151                    }
8152                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8153                        self.advance();
8154                    }
8155                    other => {
8156                        return Err(self.err(alloc::format!(
8157                            "expected ONLY or WRITE after READ, got {other:?}"
8158                        )));
8159                    }
8160                }
8161            } else if matches!(self.peek(), Token::Not) {
8162                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8163                self.advance();
8164                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8165                    return Err(self.err(alloc::format!(
8166                        "expected DEFERRABLE after NOT, got {:?}",
8167                        self.peek()
8168                    )));
8169                }
8170                self.advance();
8171            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8172            {
8173                self.advance();
8174            } else {
8175                break;
8176            }
8177            // Optional comma between modes.
8178            if matches!(self.peek(), Token::Comma) {
8179                self.advance();
8180            }
8181        }
8182        Ok(have_level.then_some(level))
8183    }
8184
8185    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8186        // FOR is a v6.1.2-reserved keyword (Token::For). The
8187        // other two are bare idents — they've never needed lexer
8188        // support and we keep it that way.
8189        if !matches!(self.peek(), Token::For) {
8190            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8191        }
8192        self.advance();
8193        self.expect_keyword_ident("wal")?;
8194        self.expect_keyword_ident("position")?;
8195        let pos = self.expect_u64_literal()?;
8196        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8197        {
8198            self.advance();
8199            self.expect_keyword_ident("timeout")?;
8200            Some(self.expect_u64_literal()?)
8201        } else {
8202            None
8203        };
8204        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8205    }
8206
8207    /// v6.1.7 helper — consume a `Token::Integer` and check it
8208    /// fits `u64`. WAL positions and millisecond timeouts are
8209    /// non-negative.
8210    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8211        match self.advance() {
8212            Token::Integer(n) if n >= 0 => Ok(n as u64),
8213            Token::Integer(n) => Err(ParseError {
8214                message: format!("expected non-negative integer, got {n}"),
8215                token_pos: self.consumed_pos(),
8216            }),
8217            other => Err(ParseError {
8218                message: format!("expected integer literal, got {other:?}"),
8219                token_pos: self.consumed_pos(),
8220            }),
8221        }
8222    }
8223
8224    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8225    /// ROLE '<role>' (defaults to readonly). All string slots accept
8226    /// either a quoted ident or a quoted string literal.
8227    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8228    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8229    ///
8230    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8231    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8232    /// wire role) still parses — it is a different axis from the PG attributes.
8233    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8234    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8235    /// or RESET, so the plain attribute forms keep their old path.
8236    fn peeks_db_role_setting(&self) -> bool {
8237        let mut i = self.pos + 1; // past the object's name
8238        let word = |p: usize| -> Option<String> {
8239            match self.tokens.get(p) {
8240                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8241                Some(Token::In) => Some(String::from("in")),
8242                _ => None,
8243            }
8244        };
8245        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8246            i += 3; // IN DATABASE <name>
8247        }
8248        matches!(word(i).as_deref(), Some("set" | "reset"))
8249    }
8250
8251    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8252        use crate::ast::SetDbRoleSettingStatement;
8253        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8254        // identifier, so the ordinary name reader refuses it. Same trap
8255        // as TABLE / INDEX / FULL / DEFAULT before it.
8256        let name = if matches!(self.peek(), Token::All) {
8257            self.advance();
8258            String::from("all")
8259        } else {
8260            self.expect_ident_or_string()?
8261        };
8262        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8263        let all = name.eq_ignore_ascii_case("all");
8264        let (mut database, mut role) = if is_database {
8265            (Some(name), None)
8266        } else if all {
8267            (None, None)
8268        } else {
8269            (None, Some(name))
8270        };
8271        if matches!(self.peek(), Token::In) {
8272            self.advance();
8273            self.advance(); // DATABASE
8274            database = Some(self.expect_ident_or_string()?);
8275        }
8276        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8277        self.advance(); // SET | RESET
8278        if resetting && matches!(self.peek(), Token::All) {
8279            self.advance();
8280            self.consume_until_statement_boundary();
8281            return Ok(Statement::SetDbRoleSetting(Box::new(
8282                SetDbRoleSettingStatement {
8283                    database,
8284                    role,
8285                    param: None,
8286                    value: None,
8287                },
8288            )));
8289        }
8290        let param = self.expect_ident_like()?;
8291        let value = if resetting {
8292            None
8293        } else {
8294            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8295            // KEYWORD, so the ident-only check missed it and consumed
8296            // the word itself as the value — the same trap as ALL, one
8297            // clause over.
8298            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8299                self.advance();
8300            }
8301            Some(self.take_guc_value())
8302        };
8303        self.consume_until_statement_boundary();
8304        Ok(Statement::SetDbRoleSetting(Box::new(
8305            SetDbRoleSettingStatement {
8306                database,
8307                role,
8308                param: Some(param),
8309                value,
8310            },
8311        )))
8312    }
8313
8314    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8315    /// a quoted literal loses its quotes, a bare word or number does not.
8316    fn take_guc_value(&mut self) -> String {
8317        match self.advance() {
8318            Token::String(s) => s,
8319            Token::Integer(n) => format!("{n}"),
8320            Token::Float(f) => format!("{f}"),
8321            Token::Ident(s) | Token::QuotedIdent(s) => s,
8322            other => format!("{other:?}"),
8323        }
8324    }
8325
8326    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8327        let name = self.expect_ident_or_string()?;
8328        if self.peek_keyword_ident("with") {
8329            self.advance();
8330        }
8331        let mut password = String::new();
8332        let mut role = String::new();
8333        let mut login: Option<bool> = None;
8334        let mut inherit: Option<bool> = None;
8335        let mut superuser: Option<bool> = None;
8336        // Not a `while let`: the pattern would borrow `self` across the
8337        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8338        #[allow(clippy::while_let_loop)]
8339        loop {
8340            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8341                break;
8342            };
8343            match w.to_ascii_lowercase().as_str() {
8344                "password" => {
8345                    self.advance();
8346                    password = self.expect_string_literal()?;
8347                }
8348                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8349                // is the same slot.
8350                "encrypted" => {
8351                    self.advance();
8352                    self.expect_keyword_ident("password")?;
8353                    password = self.expect_string_literal()?;
8354                }
8355                "login" => {
8356                    self.advance();
8357                    login = Some(true);
8358                }
8359                "nologin" => {
8360                    self.advance();
8361                    login = Some(false);
8362                }
8363                "inherit" => {
8364                    self.advance();
8365                    inherit = Some(true);
8366                }
8367                "noinherit" => {
8368                    self.advance();
8369                    inherit = Some(false);
8370                }
8371                "superuser" => {
8372                    self.advance();
8373                    superuser = Some(true);
8374                }
8375                "nosuperuser" => {
8376                    self.advance();
8377                    superuser = Some(false);
8378                }
8379                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8380                "role" => {
8381                    self.advance();
8382                    role = self.expect_string_literal()?;
8383                }
8384                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8385                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8386                // accepted and ignored so a pg_dump role block restores. They
8387                // gate capabilities SPG does not have.
8388                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8389                | "noreplication" | "bypassrls" | "nobypassrls" => {
8390                    self.advance();
8391                }
8392                "connection" => {
8393                    self.advance();
8394                    self.expect_keyword_ident("limit")?;
8395                    self.advance(); // the number
8396                }
8397                "valid" => {
8398                    self.advance();
8399                    self.expect_keyword_ident("until")?;
8400                    self.expect_string_literal()?;
8401                }
8402                _ => break,
8403            }
8404        }
8405        if role.is_empty() {
8406            role = "readonly".to_string();
8407        }
8408        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8409            name,
8410            password,
8411            role,
8412            login,
8413            inherit,
8414            superuser,
8415            is_user,
8416        }))
8417    }
8418
8419    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8420    /// consumed the USING / WITH CHECK keyword.
8421    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8422        if !matches!(self.peek(), Token::LParen) {
8423            return Err(self.err(alloc::format!(
8424                "expected '(' after {clause}, got {:?}",
8425                self.peek()
8426            )));
8427        }
8428        self.advance();
8429        let e = self.parse_expr(0)?;
8430        if !matches!(self.peek(), Token::RParen) {
8431            return Err(self.err(alloc::format!(
8432                "expected ')' to close {clause}, got {:?}",
8433                self.peek()
8434            )));
8435        }
8436        self.advance();
8437        Ok(e)
8438    }
8439
8440    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8441    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8442        let mut roles = Vec::new();
8443        loop {
8444            roles.push(self.expect_ident_like()?);
8445            if matches!(self.peek(), Token::Comma) {
8446                self.advance();
8447            } else {
8448                break;
8449            }
8450        }
8451        Ok(roles)
8452    }
8453
8454    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8455    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8456    /// `CREATE POLICY`.
8457    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8458        use crate::ast::PolicyCmd;
8459        let name = self.expect_ident_like()?;
8460        if !matches!(self.peek(), Token::On) {
8461            return Err(self.err(alloc::format!(
8462                "expected ON after CREATE POLICY name, got {:?}",
8463                self.peek()
8464            )));
8465        }
8466        self.advance();
8467        let table = self.expect_ident_like()?;
8468
8469        let mut permissive = true;
8470        if matches!(self.peek(), Token::As) {
8471            self.advance();
8472            let w = self.expect_ident_like()?;
8473            permissive = if w.eq_ignore_ascii_case("permissive") {
8474                true
8475            } else if w.eq_ignore_ascii_case("restrictive") {
8476                false
8477            } else {
8478                return Err(self.err(alloc::format!(
8479                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8480                )));
8481            };
8482        }
8483
8484        let mut cmd = PolicyCmd::All;
8485        if matches!(self.peek(), Token::For) {
8486            self.advance();
8487            cmd = self.parse_policy_cmd()?;
8488        }
8489
8490        let mut roles = Vec::new();
8491        if matches!(self.peek(), Token::To) {
8492            self.advance();
8493            roles = self.parse_policy_roles()?;
8494        }
8495
8496        let mut using = None;
8497        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8498        {
8499            self.advance();
8500            using = Some(self.parse_paren_expr("USING")?);
8501        }
8502
8503        let mut with_check = None;
8504        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8505        {
8506            self.advance();
8507            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8508            {
8509                return Err(self.err(alloc::format!(
8510                    "expected CHECK after WITH, got {:?}",
8511                    self.peek()
8512                )));
8513            }
8514            self.advance();
8515            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8516        }
8517
8518        // Clause-per-command matrix (PG wording).
8519        match cmd {
8520            PolicyCmd::Insert => {
8521                if using.is_some() {
8522                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8523                }
8524            }
8525            PolicyCmd::Select | PolicyCmd::Delete => {
8526                if with_check.is_some() {
8527                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8528                }
8529            }
8530            PolicyCmd::Update | PolicyCmd::All => {}
8531        }
8532
8533        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8534            name,
8535            table,
8536            permissive,
8537            cmd,
8538            roles,
8539            using,
8540            with_check,
8541        }))
8542    }
8543
8544    /// v7.39 (RLS) — the command word after `FOR`.
8545    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8546        use crate::ast::PolicyCmd;
8547        match self.peek().clone() {
8548            Token::All => {
8549                self.advance();
8550                Ok(PolicyCmd::All)
8551            }
8552            Token::Select => {
8553                self.advance();
8554                Ok(PolicyCmd::Select)
8555            }
8556            Token::Insert => {
8557                self.advance();
8558                Ok(PolicyCmd::Insert)
8559            }
8560            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8561                self.advance();
8562                Ok(PolicyCmd::Update)
8563            }
8564            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8565                self.advance();
8566                Ok(PolicyCmd::Delete)
8567            }
8568            other => Err(self.err(alloc::format!(
8569                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8570            ))),
8571        }
8572    }
8573
8574    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8575    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8576    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8577        let name = self.expect_ident_like()?;
8578        if !matches!(self.peek(), Token::On) {
8579            return Err(self.err(alloc::format!(
8580                "expected ON after ALTER POLICY name, got {:?}",
8581                self.peek()
8582            )));
8583        }
8584        self.advance();
8585        let table = self.expect_ident_like()?;
8586
8587        // RENAME TO new
8588        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8589        {
8590            self.advance();
8591            if !matches!(self.peek(), Token::To) {
8592                return Err(self.err(alloc::format!(
8593                    "expected TO after RENAME, got {:?}",
8594                    self.peek()
8595                )));
8596            }
8597            self.advance();
8598            let new = self.expect_ident_like()?;
8599            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8600                name,
8601                table,
8602                rename_to: Some(new),
8603                roles: None,
8604                using: None,
8605                with_check: None,
8606            }));
8607        }
8608
8609        let mut roles = None;
8610        if matches!(self.peek(), Token::To) {
8611            self.advance();
8612            roles = Some(self.parse_policy_roles()?);
8613        }
8614        let mut using = None;
8615        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8616        {
8617            self.advance();
8618            using = Some(self.parse_paren_expr("USING")?);
8619        }
8620        let mut with_check = None;
8621        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8622        {
8623            self.advance();
8624            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8625            {
8626                return Err(self.err(alloc::format!(
8627                    "expected CHECK after WITH, got {:?}",
8628                    self.peek()
8629                )));
8630            }
8631            self.advance();
8632            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8633        }
8634        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8635            name,
8636            table,
8637            rename_to: None,
8638            roles,
8639            using,
8640            with_check,
8641        }))
8642    }
8643
8644    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8645    /// `DROP POLICY`.
8646    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8647        let if_exists = self.consume_if_exists();
8648        let name = self.expect_ident_like()?;
8649        if !matches!(self.peek(), Token::On) {
8650            return Err(self.err(alloc::format!(
8651                "expected ON after DROP POLICY name, got {:?}",
8652                self.peek()
8653            )));
8654        }
8655        self.advance();
8656        let table = self.expect_ident_like()?;
8657        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8658            name,
8659            table,
8660            if_exists,
8661        }))
8662    }
8663}
8664fn wrap_from_leaves(
8665    e: &mut Expr,
8666    names: &[String],
8667    make: &dyn Fn(Expr) -> Expr,
8668    refs: &dyn Fn(&Expr) -> bool,
8669) {
8670    if let Expr::Column(c) = e {
8671        if c.qualifier
8672            .as_deref()
8673            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8674        {
8675            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8676            *e = make(taken);
8677        }
8678        return;
8679    }
8680    match e {
8681        Expr::Binary { lhs, rhs, .. } => {
8682            wrap_from_leaves(lhs, names, make, refs);
8683            wrap_from_leaves(rhs, names, make, refs);
8684        }
8685        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8686            wrap_from_leaves(expr, names, make, refs)
8687        }
8688        Expr::FunctionCall { args, .. } => {
8689            for a in args.iter_mut() {
8690                wrap_from_leaves(a, names, make, refs);
8691            }
8692        }
8693        Expr::Case {
8694            operand,
8695            branches,
8696            else_branch,
8697        } => {
8698            if let Some(o) = operand.as_deref_mut() {
8699                wrap_from_leaves(o, names, make, refs);
8700            }
8701            for (w, t) in branches.iter_mut() {
8702                wrap_from_leaves(w, names, make, refs);
8703                wrap_from_leaves(t, names, make, refs);
8704            }
8705            if let Some(el) = else_branch.as_deref_mut() {
8706                wrap_from_leaves(el, names, make, refs);
8707            }
8708        }
8709        // Compound variants the walk doesn't decompose: keep the
8710        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8711        // a source table, so nothing regresses.
8712        other => {
8713            if refs(other) {
8714                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8715                *other = make(taken);
8716            }
8717        }
8718    }
8719}
8720
8721/// v7.39 (round 241) — does this expression reference any of the FROM /
8722/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8723/// lowerings)?
8724fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8725    match e {
8726        Expr::Column(c) => c
8727            .qualifier
8728            .as_deref()
8729            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8730        Expr::Binary { lhs, rhs, .. } => {
8731            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8732        }
8733        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8734        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8735        Expr::Case {
8736            operand,
8737            branches,
8738            else_branch,
8739        } => {
8740            operand
8741                .as_deref()
8742                .is_some_and(|o| expr_refs_tables(o, names))
8743                || branches
8744                    .iter()
8745                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8746                || else_branch
8747                    .as_deref()
8748                    .is_some_and(|el| expr_refs_tables(el, names))
8749        }
8750        _ => false,
8751    }
8752}
8753
8754impl Parser {
8755    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8756    /// Caller already consumed the leading `UPDATE` ident.
8757    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8758    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8759    /// after the target name has been read. `JOIN` is a reserved token;
8760    /// the qualifiers are bare idents.
8761    fn peek_is_update_join_start(&self) -> bool {
8762        match self.peek() {
8763            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8764            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8765            Token::Join
8766            | Token::Inner
8767            | Token::Left
8768            | Token::Right
8769            | Token::Cross
8770            | Token::Full => true,
8771            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8772            Token::Ident(s) | Token::QuotedIdent(s) => {
8773                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8774            }
8775            _ => false,
8776        }
8777    }
8778
8779    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8780    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8781    /// expression on the right, and `:=` as a second spelling of `=`.
8782    ///
8783    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8784    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8785    /// and holding this loop's `Vec` + `String` locals there overflowed the
8786    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8787    #[inline(never)]
8788    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8789        let mut assigns: Vec<(String, Expr)> = Vec::new();
8790        let mut settings: Vec<(String, Expr)> = Vec::new();
8791        loop {
8792            // v7.39 (round 554) — a plain NAME here is a session
8793            // setting, not a user variable. mysqldump writes the two in
8794            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8795            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8796            // changes it — and this refused the mixture outright, so no
8797            // dump could be restored past its preamble.
8798            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8799                self.advance();
8800                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8801                    return Err(self.err(alloc::format!(
8802                        "expected `=` after {name}, got {:?}",
8803                        self.peek()
8804                    )));
8805                }
8806                self.advance();
8807                let value = self.parse_expr(0)?;
8808                settings.push((name.to_ascii_lowercase(), value));
8809                if matches!(self.peek(), Token::Comma) {
8810                    self.advance();
8811                    continue;
8812                }
8813                break;
8814            }
8815            let Token::SessionVar(raw) = self.peek().clone() else {
8816                return Err(self.err(alloc::format!(
8817                    "expected a user variable after SET, got {:?}",
8818                    self.peek()
8819                )));
8820            };
8821            if raw.starts_with("@@") {
8822                return Err(self.err(alloc::string::String::from(
8823                    "cannot mix `@@` settings with `@` user variables in one SET",
8824                )));
8825            }
8826            self.advance();
8827            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8828                return Err(self.err(alloc::format!(
8829                    "expected `=` or `:=` after {raw}, got {:?}",
8830                    self.peek()
8831                )));
8832            }
8833            self.advance();
8834            let value = self.parse_expr(0)?;
8835            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8836            if matches!(self.peek(), Token::Comma) {
8837                self.advance();
8838                continue;
8839            }
8840            break;
8841        }
8842        Ok(Statement::SetUserVars(assigns, settings))
8843    }
8844
8845    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8846        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8847        // NAMED `only` until now, which failed on `relation "only" does
8848        // not exist`. The lookahead is what keeps a table actually
8849        // called `only` working: the keyword is only a keyword when a
8850        // TABLE NAME follows it — and `SET` arrives as an identifier
8851        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8852        // for the table and die on the `=`. Measured by the pin.
8853        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8854            if s.eq_ignore_ascii_case("only"))
8855            && matches!(
8856                self.tokens.get(self.pos + 1),
8857                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8858            );
8859        if only {
8860            self.advance();
8861        }
8862        let table = self.expect_ident_like()?;
8863        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8864        // bare spelling; a bare identifier that is the SET keyword itself
8865        // is the clause, not an alias.
8866        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8867        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8868        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8869        // following JOIN a syntax error.
8870        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8871        let alias = if matches!(self.peek(), Token::As) {
8872            self.advance();
8873            Some(self.expect_ident_like()?)
8874        } else {
8875            match self.peek() {
8876                Token::Ident(s) | Token::QuotedIdent(s)
8877                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
8878                {
8879                    let a = s.clone();
8880                    self.advance();
8881                    Some(a)
8882                }
8883                _ => None,
8884            }
8885        };
8886        // v7.39 (round 420) — MySQL's multi-table UPDATE:
8887        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
8888        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
8889        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8890        // The FIRST table is the mutation target and the rest are sources —
8891        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8892        // SPG already lowers onto correlated subqueries. So rewind, let
8893        // `parse_from_clause` read the whole list (it handles aliases, comma
8894        // lists, and every JOIN form), then peel the target off the front.
8895        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8896            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8897        {
8898            // NOTE: `advance()` destroys the tokens it returns
8899            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8900            // is NOT possible — the tail is read forward, once, through the
8901            // same grammar `parse_from_clause` uses after its primary.
8902            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8903            let mut joins = self.parse_from_joins(&target_qual)?;
8904            if joins.is_empty() {
8905                return Err(self.err(alloc::string::String::from(
8906                    "multi-table UPDATE needs at least one source table",
8907                )));
8908            }
8909            let head = joins.remove(0);
8910            // A LEFT join keeps every target row (the unmatched ones see NULL
8911            // on the source side), so it must NOT get the EXISTS row filter
8912            // the inner / comma forms use.
8913            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8914            let src = FromClause {
8915                primary: head.table,
8916                joins,
8917            };
8918            (Some(src), head.on, outer)
8919        } else {
8920            (None, None, false)
8921        };
8922        self.expect_keyword_ident("set")?;
8923        let mut assignments = Vec::new();
8924        loop {
8925            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8926            // …)` — the parenthesized multi-assignment. Expressions
8927            // assign positionally; a subquery RHS clones per column
8928            // keeping only the Nth projection item.
8929            if matches!(self.peek(), Token::LParen) {
8930                self.advance();
8931                let mut cols = alloc::vec![self.expect_ident_like()?];
8932                while matches!(self.peek(), Token::Comma) {
8933                    self.advance();
8934                    cols.push(self.expect_ident_like()?);
8935                }
8936                if !matches!(self.peek(), Token::RParen) {
8937                    return Err(self.err(format!(
8938                        "expected ')' after SET column list, got {:?}",
8939                        self.peek()
8940                    )));
8941                }
8942                self.advance();
8943                if !matches!(self.peek(), Token::Eq) {
8944                    return Err(self.err(format!(
8945                        "expected `=` after SET column list, got {:?}",
8946                        self.peek()
8947                    )));
8948                }
8949                self.advance();
8950                if !matches!(self.peek(), Token::LParen) {
8951                    return Err(self.err(format!(
8952                        "expected '(' after SET (…) =, got {:?}",
8953                        self.peek()
8954                    )));
8955                }
8956                self.advance();
8957                if matches!(self.peek(), Token::Select) {
8958                    let inner = match self.parse_select_stmt()? {
8959                        Statement::Select(s) => s,
8960                        other => {
8961                            return Err(self.err(alloc::format!(
8962                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8963                            )));
8964                        }
8965                    };
8966                    if !matches!(self.peek(), Token::RParen) {
8967                        return Err(self.err(format!(
8968                            "expected ')' after SET subquery, got {:?}",
8969                            self.peek()
8970                        )));
8971                    }
8972                    self.advance();
8973                    if inner.items.len() != cols.len() {
8974                        return Err(self.err(alloc::format!(
8975                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8976                            cols.len(),
8977                            inner.items.len()
8978                        )));
8979                    }
8980                    for (i, col) in cols.into_iter().enumerate() {
8981                        let mut sub = inner.clone();
8982                        sub.items = alloc::vec![sub.items[i].clone()];
8983                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8984                    }
8985                } else {
8986                    let mut exprs = alloc::vec![self.parse_expr(0)?];
8987                    while matches!(self.peek(), Token::Comma) {
8988                        self.advance();
8989                        exprs.push(self.parse_expr(0)?);
8990                    }
8991                    if !matches!(self.peek(), Token::RParen) {
8992                        return Err(self.err(format!(
8993                            "expected ')' after SET row values, got {:?}",
8994                            self.peek()
8995                        )));
8996                    }
8997                    self.advance();
8998                    if exprs.len() != cols.len() {
8999                        return Err(self.err(alloc::format!(
9000                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9001                            cols.len(),
9002                            exprs.len()
9003                        )));
9004                    }
9005                    for (col, e) in cols.into_iter().zip(exprs) {
9006                        assignments.push((col, e));
9007                    }
9008                }
9009                if matches!(self.peek(), Token::Comma) {
9010                    self.advance();
9011                    continue;
9012                }
9013                break;
9014            }
9015            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9016            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9017            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9018            // `public.` dump qualifiers), so the qualifier has to be read off
9019            // the token stream first — otherwise `SET b.v = 888` would write
9020            // to the TARGET table's `v` while naming a source table, a
9021            // silent-wrong. A qualifier naming a SOURCE table means a
9022            // multi-TARGET update — mutating two tables in one statement —
9023            // which SPG does not model, so it is refused loudly.
9024            let set_qual: Option<String> = if mysql_from.is_some()
9025                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9026            {
9027                match self.peek() {
9028                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9029                    _ => None,
9030                }
9031            } else {
9032                None
9033            };
9034            let col = self.expect_ident_like()?;
9035            if let Some(q) = set_qual {
9036                let names_target = q.eq_ignore_ascii_case(&table)
9037                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9038                if !names_target {
9039                    return Err(self.err(alloc::format!(
9040                        "multi-table UPDATE can only assign to its first table \
9041                         ({table}); `{q}.{col}` targets another table"
9042                    )));
9043                }
9044            }
9045            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9046            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9047            // `__column_default` marker lowering just below). PG assigns to the
9048            // i-th (1-based) element, NULL-padding when i exceeds the length.
9049            if matches!(self.peek(), Token::LBracket) {
9050                self.advance();
9051                let index = self.parse_expr(0)?;
9052                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9053                // (and the open `arr[lo:]`), lowered to
9054                // `__array_assign_slice`. Only the single-subscript form
9055                // parsed before, so a slice assignment was a syntax error.
9056                let mut slice_hi: Option<Option<Expr>> = None;
9057                if matches!(self.peek(), Token::Colon) {
9058                    self.advance();
9059                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9060                        None
9061                    } else {
9062                        Some(self.parse_expr(0)?)
9063                    });
9064                }
9065                if !matches!(self.peek(), Token::RBracket) {
9066                    return Err(self.err(format!(
9067                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9068                        self.peek()
9069                    )));
9070                }
9071                self.advance();
9072                if !matches!(self.peek(), Token::Eq) {
9073                    return Err(self.err(format!(
9074                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9075                        self.peek()
9076                    )));
9077                }
9078                self.advance();
9079                let value = self.parse_expr(0)?;
9080                // PG merges several subscript writes to the same column into one
9081                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9082                // assignment to `col` rather than each overwriting the original.
9083                let existing = assignments.iter().position(|(c, _)| c == &col);
9084                let base = match existing {
9085                    Some(i) => assignments[i].1.clone(),
9086                    None => Expr::Column(ColumnName {
9087                        qualifier: None,
9088                        name: col.clone(),
9089                    }),
9090                };
9091                let assigned = match slice_hi {
9092                    None => Expr::FunctionCall {
9093                        name: "__array_assign".to_string(),
9094                        args: alloc::vec![base, index, value],
9095                    },
9096                    Some(hi) => Expr::FunctionCall {
9097                        name: "__array_assign_slice".to_string(),
9098                        args: alloc::vec![
9099                            base,
9100                            index,
9101                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9102                            value,
9103                        ],
9104                    },
9105                };
9106                match existing {
9107                    Some(i) => assignments[i].1 = assigned,
9108                    None => assignments.push((col, assigned)),
9109                }
9110                if matches!(self.peek(), Token::Comma) {
9111                    self.advance();
9112                    continue;
9113                }
9114                break;
9115            }
9116            if !matches!(self.peek(), Token::Eq) {
9117                return Err(self.err(format!(
9118                    "expected `=` after column name in UPDATE SET, got {:?}",
9119                    self.peek()
9120                )));
9121            }
9122            self.advance();
9123            // `SET col = DEFAULT` — the column's declared default;
9124            // rides out as a marker call the update executor
9125            // resolves against the schema.
9126            let value = if matches!(self.peek(), Token::Default) {
9127                self.advance();
9128                Expr::FunctionCall {
9129                    name: "__column_default".to_string(),
9130                    args: Vec::new(),
9131                }
9132            } else {
9133                self.parse_expr(0)?
9134            };
9135            assignments.push((col, value));
9136            if matches!(self.peek(), Token::Comma) {
9137                self.advance();
9138                continue;
9139            }
9140            break;
9141        }
9142        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9143        // update. Lowers onto the correlated-subquery machinery:
9144        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9145        // and each assignment that references a FROM-list table
9146        // wraps into a correlated scalar subquery
9147        // (SELECT expr FROM src WHERE cond). Equivalent for the
9148        // unique-join shape (the overwhelmingly common one); a
9149        // multi-match, which PG resolves by arbitrary pick,
9150        // surfaces as a scalar-subquery cardinality error instead
9151        // of a silent arbitrary result.
9152        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9153        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9154        // the SAME lowering below. Both spellings together is not legal in
9155        // either dialect.
9156        let from_clause = if let Some(fc) = mysql_from {
9157            if matches!(self.peek(), Token::From) {
9158                return Err(self.err(alloc::string::String::from(
9159                    "multi-table UPDATE already names its sources; drop the FROM clause",
9160                )));
9161            }
9162            Some(fc)
9163        } else if matches!(self.peek(), Token::From) {
9164            self.advance();
9165            Some(self.parse_from_clause()?)
9166        } else {
9167            None
9168        };
9169        let where_ = if matches!(self.peek(), Token::Where) {
9170            self.advance();
9171            Some(self.parse_expr(0)?)
9172        } else {
9173            None
9174        };
9175        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9176        // and the TARGET-row filter are NOT the same predicate once a LEFT
9177        // join is involved:
9178        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9179        //     one conjunction, and the whole thing filters target rows via
9180        //     EXISTS.
9181        //   * LEFT join: only the ON predicate belongs inside the source
9182        //     subquery. The WHERE still filters TARGET rows (with source
9183        //     columns read through the correlated subquery, which yields NULL
9184        //     for an unmatched row — exactly LEFT-join semantics).
9185        // Round 420 folded ON into WHERE unconditionally and then dropped the
9186        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9187        // WHERE a.id > 1` updated EVERY row.
9188        let sub_where = match (mysql_on.clone(), where_.clone()) {
9189            _ if mysql_outer => mysql_on.clone(),
9190            (Some(on), Some(w)) => Some(Expr::Binary {
9191                lhs: Box::new(on),
9192                op: crate::ast::BinOp::And,
9193                rhs: Box::new(w),
9194            }),
9195            (Some(on), None) => Some(on),
9196            (None, w) => w,
9197        };
9198        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9199        // has no such clause on UPDATE, so this is accepted only under the
9200        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9201        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9202        let mut returning = self.parse_optional_returning()?;
9203        // v7.39 (round 533) — kept for the engine, which can resolve the
9204        // UNQUALIFIED leaves this lowering has to leave alone.
9205        let from_sources = from_clause.as_ref().map(|fc| {
9206            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9207                from: fc.clone(),
9208                sub_where: sub_where.clone(),
9209            })
9210        });
9211        let (assignments, where_) = if let Some(fc) = from_clause {
9212            let names: Vec<String> = core::iter::once(&fc.primary)
9213                .chain(fc.joins.iter().map(|j| &j.table))
9214                .flat_map(|t| {
9215                    t.alias
9216                        .clone()
9217                        .into_iter()
9218                        .chain(core::iter::once(t.name.clone()))
9219                })
9220                .collect();
9221            let refs_list = |e: &Expr| -> bool {
9222                fn walk(e: &Expr, names: &[String]) -> bool {
9223                    match e {
9224                        Expr::Column(c) => c
9225                            .qualifier
9226                            .as_deref()
9227                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9228                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9229                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9230                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9231                        Expr::Case {
9232                            operand,
9233                            branches,
9234                            else_branch,
9235                        } => {
9236                            operand.as_deref().is_some_and(|o| walk(o, names))
9237                                || branches
9238                                    .iter()
9239                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9240                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9241                        }
9242                        _ => false,
9243                    }
9244                }
9245                walk(e, &names)
9246            };
9247            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9248                locking: None,
9249                ctes: Vec::new(),
9250                distinct: false,
9251                distinct_on: Vec::new(),
9252                items,
9253                from: Some(fc.clone()),
9254                where_: sub_where.clone(),
9255                group_by: None,
9256                group_by_all: false,
9257                having: None,
9258                unions: Vec::new(),
9259                order_by: Vec::new(),
9260                limit: None,
9261                offset: None,
9262                limit_with_ties: false,
9263                window_check_exprs: Vec::new(),
9264            };
9265            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9266            // assignment RHS with a correlated scalar subquery, instead of
9267            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9268            // column reference (`SET v = v + u.bonus`, where `v` is the target
9269            // table's column) inside a subquery whose FROM only has the source
9270            // table, so the unqualified `v` resolved against the source and
9271            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9272            // context — where they belong — fixes it; only the source columns
9273            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9274            // compound variants the leaf-walk doesn't decompose.
9275            let make_subq = |inner: Expr| {
9276                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9277                    expr: inner,
9278                    alias: None,
9279                }])))
9280            };
9281            let assignments = assignments
9282                .into_iter()
9283                .map(|(col, mut expr)| {
9284                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9285                    (col, expr)
9286                })
9287                .collect();
9288            let exists = Expr::Exists {
9289                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9290                    expr: Expr::Literal(Literal::Integer(1)),
9291                    alias: None,
9292                }])),
9293                negated: false,
9294            };
9295            // v7.39 (round 241) — RETURNING may reference the FROM-list
9296            // tables too (`RETURNING emp.id, dept.name`); the same
9297            // leaf-to-correlated-subquery lowering the assignments get.
9298            // Without it the qualifier died at eval with "unknown table
9299            // qualifier". (RETURNING was parsed before this block — the
9300            // lowering is a pure AST transformation.)
9301            if let Some(items) = returning.as_mut() {
9302                for item in items.iter_mut() {
9303                    if let SelectItem::Expr { expr, .. } = item {
9304                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9305                    }
9306                }
9307            }
9308            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9309            // EVERY matching target row: it gets no EXISTS filter, but the
9310            // caller's WHERE still applies, with source columns read through
9311            // the correlated subquery (NULL when unmatched — LEFT-join
9312            // semantics). `sub_where` above already excluded the WHERE from
9313            // the source subquery for this case.
9314            if mysql_outer {
9315                let mut outer = where_;
9316                if let Some(w) = outer.as_mut() {
9317                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9318                }
9319                (assignments, outer)
9320            } else {
9321                (assignments, Some(exists))
9322            }
9323        } else {
9324            (assignments, where_)
9325        };
9326        Ok(Statement::Update(crate::ast::UpdateStatement {
9327            ctes: Vec::new(),
9328            table,
9329            only,
9330            alias,
9331            assignments,
9332            from_sources,
9333            where_,
9334            order_limit: update_order_limit,
9335            returning,
9336        }))
9337    }
9338
9339    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9340    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9341    /// clause and its meaning are identical, so both call this rather than
9342    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9343    /// legal. PG has no such clause on either statement, so it is read only
9344    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9345    /// errors.
9346    ///
9347    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9348    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9349    /// stack in round 430.
9350    #[inline(never)]
9351    fn parse_mysql_dml_order_limit(
9352        &mut self,
9353        what: &str,
9354    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9355        if !self.mysql_dialect {
9356            return Ok(None);
9357        }
9358        let order_by = self.parse_order_by_keys()?;
9359        let limit = if matches!(self.peek(), Token::Limit) {
9360            self.advance();
9361            let tok = self.advance();
9362            let Token::Integer(n) = tok else {
9363                return Err(self.err(alloc::format!(
9364                    "expected integer after {what} LIMIT, got {tok:?}"
9365                )));
9366            };
9367            // MySQL rejects the `LIMIT offset, count` form here — only a
9368            // single row count is legal on a DML statement.
9369            if matches!(self.peek(), Token::Comma) {
9370                return Err(self.err(alloc::format!(
9371                    "{what} LIMIT takes a row count, not an offset"
9372                )));
9373            }
9374            let n = u32::try_from(n)
9375                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9376            Some(n)
9377        } else {
9378            None
9379        };
9380        if order_by.is_empty() && limit.is_none() {
9381            return Ok(None);
9382        }
9383        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9384            order_by,
9385            limit,
9386        })))
9387    }
9388
9389    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9390    /// the leading `DELETE` ident.
9391    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9392        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9393        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9394        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9395        // parse here; it reaches the existing USING path with the target
9396        // repeated in the list, which the source-list peel below handles.)
9397        // More than one name is a multi-TARGET delete, which SPG does not
9398        // model; it is refused rather than half-applied.
9399        let mysql_pre_target: Option<String> =
9400            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9401                let first = self.expect_ident_like()?;
9402                if matches!(self.peek(), Token::Comma) {
9403                    return Err(self.err(alloc::format!(
9404                        "multi-table DELETE can only delete from one table; \
9405                     `DELETE {first}, …` names several"
9406                    )));
9407                }
9408                Some(first)
9409            } else {
9410                None
9411            };
9412        if !matches!(self.peek(), Token::From) {
9413            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9414        }
9415        self.advance();
9416        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9417        // lookahead as the UPDATE spelling.
9418        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9419            if s.eq_ignore_ascii_case("only"))
9420            && matches!(
9421                self.tokens.get(self.pos + 1),
9422                Some(Token::Ident(_) | Token::QuotedIdent(_))
9423            );
9424        if only {
9425            self.advance();
9426        }
9427        let table = self.expect_ident_like()?;
9428        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9429        // spelling must not swallow the clause keywords that can follow
9430        // the target.
9431        let alias = if matches!(self.peek(), Token::As) {
9432            self.advance();
9433            Some(self.expect_ident_like()?)
9434        } else {
9435            match self.peek() {
9436                Token::Ident(s) | Token::QuotedIdent(s)
9437                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9438                {
9439                    let a = s.clone();
9440                    self.advance();
9441                    Some(a)
9442                }
9443                _ => None,
9444            }
9445        };
9446        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9447        // through the SAME join grammar the FROM clause uses (see the
9448        // `advance()`-destroys-tokens note on `parse_from_joins`).
9449        let mut mysql_on: Option<Expr> = None;
9450        let mut mysql_outer = false;
9451        let mysql_using = if mysql_pre_target.is_some()
9452            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9453        {
9454            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9455            let mut joins = self.parse_from_joins(&target_qual)?;
9456            if joins.is_empty() {
9457                return Err(self.err(alloc::string::String::from(
9458                    "multi-table DELETE needs at least one source table",
9459                )));
9460            }
9461            let head = joins.remove(0);
9462            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9463            mysql_on = head.on;
9464            Some(FromClause {
9465                primary: head.table,
9466                joins,
9467            })
9468        } else {
9469            None
9470        };
9471        // The pre-FROM target must be the table the FROM names (or its
9472        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9473        // is not the scan target.
9474        if let Some(t) = &mysql_pre_target {
9475            let names_target = t.eq_ignore_ascii_case(&table)
9476                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9477            if !names_target {
9478                return Err(self.err(alloc::format!(
9479                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9480                )));
9481            }
9482        }
9483        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9484        // delete. Same lowering as UPDATE … FROM: the WHERE
9485        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9486        // target row by the correlated machinery.
9487        let using_clause = if let Some(fc) = mysql_using {
9488            Some(fc)
9489        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9490            self.advance();
9491            let mut fc = self.parse_from_clause()?;
9492            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9493            // repeats the TARGET as the first USING entry (PG's spelling
9494            // lists only the extra sources). Peel it so the source subquery
9495            // does not re-scan — and shadow — the target table.
9496            let primary_is_target =
9497                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9498            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9499                let head = fc.joins.remove(0);
9500                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9501                mysql_on = head.on;
9502                fc = FromClause {
9503                    primary: head.table,
9504                    joins: fc.joins,
9505                };
9506            }
9507            Some(fc)
9508        } else {
9509            None
9510        };
9511        let where_ = if matches!(self.peek(), Token::Where) {
9512            self.advance();
9513            Some(self.parse_expr(0)?)
9514        } else {
9515            None
9516        };
9517        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9518        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9519        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9520        let mut returning = self.parse_optional_returning()?;
9521        let where_ = if let Some(fc) = using_clause {
9522            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9523            // a USING-table reference in RETURNING becomes a correlated
9524            // scalar subquery over the USING list.
9525            let names: Vec<String> = core::iter::once(&fc.primary)
9526                .chain(fc.joins.iter().map(|j| &j.table))
9527                .flat_map(|t| {
9528                    t.alias
9529                        .clone()
9530                        .into_iter()
9531                        .chain(core::iter::once(t.name.clone()))
9532                })
9533                .collect();
9534            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9535            // join filters the SOURCE subquery on the ON predicate alone and
9536            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9537            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9538            // rows); every other form folds ON and WHERE into one EXISTS.
9539            let sub_where = match (mysql_on.clone(), where_.clone()) {
9540                _ if mysql_outer => mysql_on.clone(),
9541                (Some(on), Some(w)) => Some(Expr::Binary {
9542                    lhs: Box::new(on),
9543                    op: crate::ast::BinOp::And,
9544                    rhs: Box::new(w),
9545                }),
9546                (Some(on), None) => Some(on),
9547                (None, w) => w,
9548            };
9549            let exists_where = sub_where.clone();
9550            let sub_fc = fc.clone();
9551            let make_subq = move |leaf: Expr| -> Expr {
9552                Expr::ScalarSubquery(Box::new(SelectStatement {
9553                    locking: None,
9554                    ctes: Vec::new(),
9555                    distinct: false,
9556                    distinct_on: Vec::new(),
9557                    items: alloc::vec![SelectItem::Expr {
9558                        expr: leaf,
9559                        alias: None,
9560                    }],
9561                    from: Some(sub_fc.clone()),
9562                    where_: sub_where.clone(),
9563                    group_by: None,
9564                    group_by_all: false,
9565                    having: None,
9566                    unions: Vec::new(),
9567                    order_by: Vec::new(),
9568                    limit: None,
9569                    offset: None,
9570                    limit_with_ties: false,
9571                    window_check_exprs: Vec::new(),
9572                }))
9573            };
9574            let refs = |e: &Expr| expr_refs_tables(e, &names);
9575            if let Some(items) = returning.as_mut() {
9576                for item in items.iter_mut() {
9577                    if let SelectItem::Expr { expr, .. } = item {
9578                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9579                    }
9580                }
9581            }
9582            // A LEFT join deletes the target rows the WHERE selects, reading
9583            // source columns through the correlated subquery (NULL when
9584            // unmatched); no EXISTS row filter.
9585            if mysql_outer {
9586                let mut outer = where_;
9587                if let Some(w) = outer.as_mut() {
9588                    wrap_from_leaves(w, &names, &make_subq, &refs);
9589                }
9590                outer
9591            } else {
9592                Some(Expr::Exists {
9593                    subquery: Box::new(SelectStatement {
9594                        locking: None,
9595                        ctes: Vec::new(),
9596                        distinct: false,
9597                        distinct_on: Vec::new(),
9598                        items: alloc::vec![SelectItem::Expr {
9599                            expr: Expr::Literal(Literal::Integer(1)),
9600                            alias: None,
9601                        }],
9602                        from: Some(fc),
9603                        where_: exists_where,
9604                        group_by: None,
9605                        group_by_all: false,
9606                        having: None,
9607                        unions: Vec::new(),
9608                        order_by: Vec::new(),
9609                        limit: None,
9610                        offset: None,
9611                        limit_with_ties: false,
9612                        window_check_exprs: Vec::new(),
9613                    }),
9614                    negated: false,
9615                })
9616            }
9617        } else {
9618            where_
9619        };
9620        Ok(Statement::Delete(crate::ast::DeleteStatement {
9621            ctes: Vec::new(),
9622            table,
9623            only,
9624            alias,
9625            where_,
9626            order_limit: delete_order_limit,
9627            returning,
9628        }))
9629    }
9630
9631    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9632    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9633    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9634    /// keyword. v7.17 surface:
9635    ///   * source: table reference (subquery source is a follow-up)
9636    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9637    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9638    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9639    ///     order
9640    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9641        // INTO
9642        let is_into_kw = matches!(self.peek(), Token::Into)
9643            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9644        if !is_into_kw {
9645            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9646        }
9647        self.advance();
9648        let target = self.expect_ident_like()?;
9649        // Optional alias — bare ident before USING.
9650        let target_alias = match self.peek() {
9651            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9652                Some(self.expect_ident_like()?)
9653            }
9654            _ => None,
9655        };
9656        // USING
9657        let is_using_kw = matches!(
9658            self.peek(),
9659            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9660        );
9661        if !is_using_kw {
9662            return Err(self.err(format!(
9663                "expected USING after MERGE INTO target, got {:?}",
9664                self.peek()
9665            )));
9666        }
9667        self.advance();
9668        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9669        // <table> [alias]`. PG requires an alias after a subquery source.
9670        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9671            self.advance(); // (
9672            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9673            // constant-SELECT lowering the derived-table parser uses
9674            // (PG deletes through this form; it was a parse error).
9675            let inner = if matches!(self.peek(), Token::Values) {
9676                self.advance(); // VALUES
9677                Statement::Select(self.parse_values_rows_body()?)
9678            } else {
9679                self.parse_select_stmt()?
9680            };
9681            match self.advance() {
9682                Token::RParen => {}
9683                other => {
9684                    return Err(self.err(format!(
9685                        "expected ')' after MERGE USING subquery, got {other:?}"
9686                    )));
9687                }
9688            }
9689            let Statement::Select(sub) = inner else {
9690                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9691            };
9692            (String::new(), Some(Box::new(sub)))
9693        } else {
9694            (self.expect_ident_like()?, None)
9695        };
9696        let source_alias = match self.peek() {
9697            Token::Ident(s) | Token::QuotedIdent(s)
9698                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9699            {
9700                Some(self.expect_ident_like()?)
9701            }
9702            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9703                self.advance(); // AS
9704                Some(self.expect_ident_like()?)
9705            }
9706            _ => None,
9707        };
9708        // v7.39 (round 768, F31-D5) — optional positional column-alias
9709        // list after the source alias (`s(id, v)`).
9710        let mut source_column_aliases: Vec<String> = Vec::new();
9711        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9712            self.advance();
9713            loop {
9714                source_column_aliases.push(self.expect_ident_like()?);
9715                match self.peek() {
9716                    Token::Comma => {
9717                        self.advance();
9718                    }
9719                    Token::RParen => {
9720                        self.advance();
9721                        break;
9722                    }
9723                    other => {
9724                        return Err(self.err(format!(
9725                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9726                        )));
9727                    }
9728                }
9729            }
9730        }
9731        if source_select.is_some() && source_alias.is_none() {
9732            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9733        }
9734        // ON
9735        if !matches!(self.peek(), Token::On) {
9736            return Err(self.err(format!(
9737                "expected ON after MERGE … USING source, got {:?}",
9738                self.peek()
9739            )));
9740        }
9741        self.advance();
9742        let on = self.parse_expr(0)?;
9743        // One or more WHEN clauses.
9744        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9745        loop {
9746            let is_when_kw = matches!(
9747                self.peek(),
9748                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9749            );
9750            if !is_when_kw {
9751                break;
9752            }
9753            self.advance(); // WHEN
9754            // [NOT] MATCHED
9755            let matched = if matches!(self.peek(), Token::Not) {
9756                self.advance();
9757                crate::ast::MergeMatched::NotMatched
9758            } else {
9759                crate::ast::MergeMatched::Matched
9760            };
9761            let is_matched_kw = matches!(
9762                self.peek(),
9763                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9764            );
9765            if !is_matched_kw {
9766                return Err(self.err(format!(
9767                    "expected MATCHED in WHEN clause, got {:?}",
9768                    self.peek()
9769                )));
9770            }
9771            self.advance();
9772            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9773            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9774            // to fire for target rows no source row matches.
9775            let mut matched = matched;
9776            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9777                self.advance();
9778                match self.peek() {
9779                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9780                        self.advance();
9781                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9782                    }
9783                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9784                        self.advance();
9785                    }
9786                    other => {
9787                        return Err(self.err(format!(
9788                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9789                        )));
9790                    }
9791                }
9792            }
9793            // Optional AND <expr>
9794            let condition = if matches!(self.peek(), Token::And) {
9795                self.advance();
9796                Some(self.parse_expr(0)?)
9797            } else {
9798                None
9799            };
9800            // THEN
9801            let is_then_kw = matches!(
9802                self.peek(),
9803                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9804            );
9805            if !is_then_kw {
9806                return Err(self.err(format!(
9807                    "expected THEN in WHEN clause, got {:?}",
9808                    self.peek()
9809                )));
9810            }
9811            self.advance();
9812            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9813            let action = match self.peek().clone() {
9814                Token::Insert => {
9815                    self.advance();
9816                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9817                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9818                    // VALUES (…)` omits it and fills every column in declaration
9819                    // order. PG accepts this; SPG used to require the list.
9820                    let mut columns: Vec<String> = Vec::new();
9821                    if matches!(self.peek(), Token::LParen) {
9822                        self.advance();
9823                        loop {
9824                            columns.push(self.expect_ident_like()?);
9825                            if matches!(self.peek(), Token::Comma) {
9826                                self.advance();
9827                                continue;
9828                            }
9829                            break;
9830                        }
9831                        if !matches!(self.peek(), Token::RParen) {
9832                            return Err(self.err(format!(
9833                                "expected ')' after INSERT column list, got {:?}",
9834                                self.peek()
9835                            )));
9836                        }
9837                        self.advance();
9838                    }
9839                    // VALUES (...)
9840                    if !matches!(self.peek(), Token::Values) {
9841                        return Err(self.err(format!(
9842                            "expected VALUES in MERGE INSERT, got {:?}",
9843                            self.peek()
9844                        )));
9845                    }
9846                    self.advance();
9847                    if !matches!(self.peek(), Token::LParen) {
9848                        return Err(self.err(format!(
9849                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
9850                            self.peek()
9851                        )));
9852                    }
9853                    self.advance();
9854                    let mut values: Vec<crate::ast::Expr> = Vec::new();
9855                    loop {
9856                        values.push(self.parse_expr(0)?);
9857                        if matches!(self.peek(), Token::Comma) {
9858                            self.advance();
9859                            continue;
9860                        }
9861                        break;
9862                    }
9863                    if !matches!(self.peek(), Token::RParen) {
9864                        return Err(self.err(format!(
9865                            "expected ')' after MERGE INSERT values, got {:?}",
9866                            self.peek()
9867                        )));
9868                    }
9869                    self.advance();
9870                    // Empty column list = positional into every column, so the
9871                    // count is checked against the table arity at execution.
9872                    if !columns.is_empty() && columns.len() != values.len() {
9873                        return Err(self.err(format!(
9874                            "MERGE INSERT column count ({}) ≠ value count ({})",
9875                            columns.len(),
9876                            values.len()
9877                        )));
9878                    }
9879                    crate::ast::MergeAction::Insert { columns, values }
9880                }
9881                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9882                    self.advance();
9883                    // SET
9884                    let is_set_kw = matches!(
9885                        self.peek(),
9886                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9887                    );
9888                    if !is_set_kw {
9889                        return Err(self.err(format!(
9890                            "expected SET after UPDATE in MERGE, got {:?}",
9891                            self.peek()
9892                        )));
9893                    }
9894                    self.advance();
9895                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9896                    loop {
9897                        let col = self.expect_ident_like()?;
9898                        if !matches!(self.peek(), Token::Eq) {
9899                            return Err(self.err(format!(
9900                                "expected '=' in MERGE UPDATE assignment, got {:?}",
9901                                self.peek()
9902                            )));
9903                        }
9904                        self.advance();
9905                        let expr = self.parse_expr(0)?;
9906                        assignments.push((col, expr));
9907                        if matches!(self.peek(), Token::Comma) {
9908                            self.advance();
9909                            continue;
9910                        }
9911                        break;
9912                    }
9913                    crate::ast::MergeAction::Update { assignments }
9914                }
9915                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9916                    self.advance();
9917                    crate::ast::MergeAction::Delete
9918                }
9919                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9920                    self.advance();
9921                    let is_nothing_kw = matches!(
9922                        self.peek(),
9923                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9924                    );
9925                    if !is_nothing_kw {
9926                        return Err(self.err(format!(
9927                            "expected NOTHING after DO in MERGE clause, got {:?}",
9928                            self.peek()
9929                        )));
9930                    }
9931                    self.advance();
9932                    crate::ast::MergeAction::DoNothing
9933                }
9934                other => {
9935                    return Err(self.err(format!(
9936                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9937                    )));
9938                }
9939            };
9940            // PG's grammar simply has no INSERT production under BY SOURCE
9941            // (a target row already exists there) — same syntax error.
9942            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9943                && matches!(action, crate::ast::MergeAction::Insert { .. })
9944            {
9945                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9946            }
9947            clauses.push(crate::ast::MergeWhenClause {
9948                matched,
9949                condition,
9950                action,
9951            });
9952        }
9953        if clauses.is_empty() {
9954            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9955        }
9956        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9957        // unconditional (no `AND`) WHEN of the same match kind: it could
9958        // never fire. Check per match kind in clause order.
9959        let mut seen_unconditional_matched = false;
9960        let mut seen_unconditional_not_matched = false;
9961        let mut seen_unconditional_by_source = false;
9962        for c in &clauses {
9963            let seen = match c.matched {
9964                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9965                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9966                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9967            };
9968            if *seen {
9969                return Err(self.err(String::from(
9970                    "unreachable WHEN clause specified after unconditional WHEN clause",
9971                )));
9972            }
9973            if c.condition.is_none() {
9974                *seen = true;
9975            }
9976        }
9977        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9978        let returning = self.parse_optional_returning()?;
9979        Ok(Statement::Merge(crate::ast::MergeStatement {
9980            // Attached by `parse_with_cte_then_select` when the MERGE
9981            // heads a WITH clause (round 149).
9982            ctes: Vec::new(),
9983            target,
9984            target_alias,
9985            source,
9986            source_alias,
9987            source_select,
9988            source_column_aliases,
9989            on,
9990            clauses,
9991            returning,
9992        }))
9993    }
9994
9995    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9996    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9997    /// as SELECT, so `RETURNING *`, `RETURNING col`,
9998    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9999    fn parse_optional_returning(
10000        &mut self,
10001    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10002        let is_returning_kw = matches!(
10003            self.peek(),
10004            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10005        );
10006        if !is_returning_kw {
10007            return Ok(None);
10008        }
10009        self.advance();
10010        let mut items = Vec::new();
10011        loop {
10012            items.push(self.parse_select_item()?);
10013            if matches!(self.peek(), Token::Comma) {
10014                self.advance();
10015                continue;
10016            }
10017            break;
10018        }
10019        Ok(Some(items))
10020    }
10021
10022    /// v6.0.4 — parse the tail of an ALTER statement after the
10023    /// leading `ALTER` keyword has been consumed. Only one form is
10024    /// supported in v6.0.4:
10025    ///
10026    /// ```text
10027    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10028    /// ```
10029    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10030        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10031        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10032        // exclusion) is accepted by stripping the `ONLY` keyword
10033        // before the table parse.
10034        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10035        // and the long PG-dump tail are accepted as no-ops.
10036        match self.advance() {
10037            Token::Index => {}
10038            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10039            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10040            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10041            Token::Table => {
10042                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10043                    self.advance();
10044                }
10045                return self.parse_alter_table_after_keyword();
10046            }
10047            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10048                return self.parse_alter_policy_after_keyword();
10049            }
10050            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10051                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10052                    self.advance();
10053                }
10054                return self.parse_alter_table_after_keyword();
10055            }
10056            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10057            // of the silent-noop tail.
10058            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10059                return self.parse_alter_sequence_after_keyword();
10060            }
10061            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10062            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10063            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10064            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10065                // NB: the match arm consumed `TYPE` via self.advance(); the
10066                // cursor is now at the type name — do NOT advance again.
10067                let type_name = self.expect_ident_like()?;
10068                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10069                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10070                if is_add_value {
10071                    self.advance(); // ADD
10072                    self.advance(); // VALUE
10073                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10074                    // IF/EXISTS as identifiers.
10075                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10076                    {
10077                        let n1 = self.tokens.get(self.pos + 1);
10078                        let n2 = self.tokens.get(self.pos + 2);
10079                        if matches!(n1, Some(Token::Not))
10080                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10081                        {
10082                            self.advance();
10083                            self.advance();
10084                            self.advance();
10085                            true
10086                        } else {
10087                            false
10088                        }
10089                    } else {
10090                        false
10091                    };
10092                    let label = self.expect_string_literal()?;
10093                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10094                    {
10095                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10096                        self.advance();
10097                        let anchor = self.expect_string_literal()?;
10098                        Some((is_before, anchor))
10099                    } else {
10100                        None
10101                    };
10102                    return Ok(Statement::AlterTypeAddValue {
10103                        type_name,
10104                        label,
10105                        if_not_exists,
10106                        position,
10107                    });
10108                }
10109                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10110                // Used to fall into the no-op tail below: accepted, silently
10111                // ignored. `RENAME TO <newtype>` keeps falling through.
10112                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10113                    && matches!(
10114                        self.tokens.get(self.pos + 1),
10115                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10116                    )
10117                {
10118                    self.advance(); // RENAME
10119                    self.advance(); // VALUE
10120                    let old = self.expect_string_literal()?;
10121                    if matches!(self.peek(), Token::To) {
10122                        self.advance();
10123                    } else {
10124                        self.expect_keyword_ident("to")?;
10125                    }
10126                    let new = self.expect_string_literal()?;
10127                    return Ok(Statement::AlterTypeRenameValue {
10128                        type_name,
10129                        old,
10130                        new,
10131                    });
10132                }
10133                // Other ALTER TYPE forms — the ACTION stays a no-op
10134                // (pg_dump tail), but v7.39 (round 708) the NAME is
10135                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10136                // success for a type that does not exist.
10137                self.consume_until_statement_boundary();
10138                return Ok(Statement::ValidateOnly {
10139                    kind: crate::ast::ValidateOnlyKind::TypeName,
10140                    names: alloc::vec![type_name],
10141                });
10142            }
10143            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10144            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10145            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10146            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10147            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10148            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10149            // pg_dump no-op list below: every form used to report success
10150            // and change nothing, which is worse than refusing outright
10151            // (a migration dropping a constraint kept rejecting data).
10152            // NOTE: the enclosing `match self.advance()` already consumed
10153            // the DOMAIN keyword, so the name is next.
10154            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10155                return self.parse_alter_domain_after_keyword();
10156            }
10157            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10158            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10159            // used to fall into the pg_dump no-op tail below, so a DBA
10160            // setting a per-role default was told it worked and nothing
10161            // happened. Intercepted here, BEFORE that tail.
10162            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10163            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10164            // interception below exists: swallowed with the no-op tail, an
10165            // unknown parameter name was ACCEPTED where PG18 answers
10166            // `unrecognized configuration parameter`. SPG applies nothing
10167            // either way — there is no postgresql.auto.conf — but it now
10168            // says so about a name it does not know.
10169            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10170                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10171                // already consumed here. An extra advance eats the SET and
10172                // the parameter name is never seen — which is exactly the
10173                // bug a panic in this branch disproved: the branch WAS on
10174                // the path, the reading of it was wrong.
10175                let mut parameter = None;
10176                // SET <name> … | RESET <name> | RESET ALL
10177                if matches!(self.peek(), Token::Ident(k)
10178                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10179                {
10180                    self.advance();
10181                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10182                        && !n.eq_ignore_ascii_case("all")
10183                    {
10184                        self.advance();
10185                        // A dotted GUC (`plpgsql.check_asserts`) is two
10186                        // tokens; keep the whole name.
10187                        let mut full = n;
10188                        while matches!(self.peek(), Token::Dot) {
10189                            self.advance();
10190                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10191                                full.push('.');
10192                                full.push_str(&t);
10193                            }
10194                        }
10195                        parameter = Some(full);
10196                    }
10197                }
10198                self.consume_until_statement_boundary();
10199                return Ok(Statement::AlterSystem { parameter });
10200            }
10201            Token::Ident(s) | Token::QuotedIdent(s)
10202                if matches!(
10203                    s.to_ascii_lowercase().as_str(),
10204                    "role" | "user" | "database"
10205                ) && self.peeks_db_role_setting() =>
10206            {
10207                let is_database = s.eq_ignore_ascii_case("database");
10208                return self.parse_db_role_setting(is_database);
10209            }
10210            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10211            // (the non-SET forms; SET/RESET took the branch above). The
10212            // attributes still no-op — recorded, and the ignored PASSWORD
10213            // is ledgered as its own follow-up — but the ROLE is validated:
10214            // any name was accepted for a role that does not exist.
10215            Token::Ident(s) | Token::QuotedIdent(s)
10216                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10217            {
10218                // NB: the enclosing `match self.advance()` already consumed
10219                // ROLE/USER — the round-695 trap, hit again in this round's
10220                // first draft (the name was eaten and WITH parsed as the
10221                // role). The cursor is at the name.
10222                let name = self.expect_ident_or_string()?;
10223                // v7.39 (round 750) — scan the attribute tail for
10224                // PASSWORD. Everything else stays a recorded no-op, but
10225                // a dropped credential rotation is a SECURITY bug:
10226                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10227                // changed nothing, so the old password kept working.
10228                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10229                // NULL` clears the credential.
10230                let mut password: Option<Option<String>> = None;
10231                loop {
10232                    match self.peek() {
10233                        Token::Semicolon | Token::Eof => break,
10234                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10235                            self.advance();
10236                            match self.advance() {
10237                                Token::String(p) => password = Some(Some(p)),
10238                                Token::Null => password = Some(None),
10239                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10240                                    password = Some(None);
10241                                }
10242                                other => {
10243                                    return Err(self.err(alloc::format!(
10244                                        "expected password string or NULL after PASSWORD, got {other:?}"
10245                                    )));
10246                                }
10247                            }
10248                        }
10249                        _ => {
10250                            self.advance();
10251                        }
10252                    }
10253                }
10254                if name.eq_ignore_ascii_case("all") {
10255                    // `ALTER ROLE ALL …` names every role; nothing to check.
10256                    return Ok(Statement::Empty);
10257                }
10258                if let Some(pw) = password {
10259                    return Ok(Statement::AlterRolePassword { name, password: pw });
10260                }
10261                return Ok(Statement::ValidateOnly {
10262                    kind: crate::ast::ValidateOnlyKind::RoleName,
10263                    names: alloc::vec![name],
10264                });
10265            }
10266            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10267            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10268            // list far enough to validate the NAME; the actions still no-op.
10269            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10270            // models none of them and their dumps are rare.)
10271            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10272                let name = self.expect_ident_or_string()?;
10273                self.consume_until_statement_boundary();
10274                return Ok(Statement::ValidateOnly {
10275                    kind: crate::ast::ValidateOnlyKind::CollationName,
10276                    names: alloc::vec![name],
10277                });
10278            }
10279            Token::Ident(s) | Token::QuotedIdent(s)
10280                if s.eq_ignore_ascii_case("text")
10281                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10282                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10283            {
10284                self.advance(); // SEARCH
10285                self.advance(); // CONFIGURATION
10286                let name = self.expect_ident_like()?;
10287                self.consume_until_statement_boundary();
10288                return Ok(Statement::ValidateOnly {
10289                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10290                    names: alloc::vec![name],
10291                });
10292            }
10293            Token::Ident(s) | Token::QuotedIdent(s)
10294                if s.eq_ignore_ascii_case("event")
10295                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10296            {
10297                self.advance(); // TRIGGER
10298                let name = self.expect_ident_like()?;
10299                self.consume_until_statement_boundary();
10300                return Ok(Statement::ValidateOnly {
10301                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10302                    names: alloc::vec![name],
10303                });
10304            }
10305            Token::Ident(s) | Token::QuotedIdent(s)
10306                if s.eq_ignore_ascii_case("large")
10307                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10308            {
10309                self.advance(); // OBJECT
10310                let oid = match self.advance() {
10311                    Token::Integer(n) => alloc::format!("{n}"),
10312                    other => {
10313                        return Err(
10314                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10315                        );
10316                    }
10317                };
10318                self.consume_until_statement_boundary();
10319                return Ok(Statement::ValidateOnly {
10320                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10321                    names: alloc::vec![oid],
10322                });
10323            }
10324            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10325            // argument-list parse as DROP AGGREGATE (round 707); the
10326            // action no-ops, the existence check is real.
10327            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10328                // Same round-695 trap as above: AGGREGATE is already
10329                // consumed; the cursor is at the name.
10330                let name = self.expect_ident_like()?;
10331                let mut names = alloc::vec![name];
10332                if matches!(self.peek(), Token::LParen) {
10333                    self.advance();
10334                    loop {
10335                        match self.peek().clone() {
10336                            Token::RParen => {
10337                                self.advance();
10338                                break;
10339                            }
10340                            Token::Star => {
10341                                self.advance();
10342                                names.push(String::from("*"));
10343                            }
10344                            Token::Comma => {
10345                                self.advance();
10346                            }
10347                            _ => {
10348                                let mut t = self.expect_ident_like()?;
10349                                while let Token::Ident(nx) = self.peek() {
10350                                    let nx = nx.clone();
10351                                    self.advance();
10352                                    t.push(' ');
10353                                    t.push_str(&nx);
10354                                }
10355                                names.push(t);
10356                            }
10357                        }
10358                    }
10359                }
10360                self.consume_until_statement_boundary();
10361                return Ok(Statement::ValidateOnly {
10362                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10363                    names,
10364                });
10365            }
10366            Token::Ident(s) | Token::QuotedIdent(s)
10367                if matches!(
10368                    s.to_ascii_lowercase().as_str(),
10369                    "view"
10370                        | "function"
10371                        | "database"
10372                        | "schema"
10373                        | "owner"
10374                        | "default"
10375                        | "extension"
10376                        | "materialized"
10377                        | "publication"
10378                        | "subscription"
10379                        // v7.37.17 (17.6 siblings) — additional ALTER
10380                        // targets pg_dump / pg_dumpall / operator DB
10381                        // migration scripts commonly emit. SPG has
10382                        // no matching machinery for any of these; the
10383                        // parser accepts + Empty-returns so pg_dump
10384                        // tail statements don't stall.
10385                        | "tablespace"
10386                        | "language"
10387                        | "operator"
10388                        | "conversion"
10389                        | "statistics"
10390                        | "server"
10391                        | "foreign"
10392                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10393                        // / TEMPLATE (CONFIGURATION intercepted above).
10394                        | "text"
10395                ) =>
10396            {
10397                self.consume_until_statement_boundary();
10398                return Ok(Statement::Empty);
10399            }
10400            other => {
10401                return Err(self.err(format!(
10402                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10403                     after ALTER, got {other:?}"
10404                )));
10405            }
10406        }
10407        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10408        // (mailrs migrate-042 ships these). The presence of an
10409        // IF EXISTS makes the subsequent name lookup tolerate
10410        // a missing index — engine returns CommandOk no-op.
10411        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10412            let next = self.tokens.get(self.pos + 1);
10413            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10414                self.advance();
10415                self.advance();
10416                true
10417            } else {
10418                false
10419            }
10420        } else {
10421            false
10422        };
10423        let name = self.expect_ident_like()?;
10424        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10425        // Detect BEFORE the REBUILD path so the existing REBUILD
10426        // arm stays untouched.
10427        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10428            self.advance();
10429            if matches!(self.peek(), Token::To) {
10430                self.advance();
10431            } else {
10432                self.expect_keyword_ident("to")?;
10433            }
10434            let new = self.expect_ident_like()?;
10435            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10436                name,
10437                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10438            }));
10439        }
10440        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10441        // A syntax error before; the index is validated, the params no-op.
10442        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10443            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10444                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10445        {
10446            self.consume_until_statement_boundary();
10447            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10448                name,
10449                target: crate::ast::AlterIndexTarget::StorageParams,
10450            }));
10451        }
10452        // REBUILD
10453        self.expect_keyword_ident("rebuild")?;
10454        // Optional: WITH (encoding = <enc>)
10455        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10456            self.advance();
10457            if !matches!(self.peek(), Token::LParen) {
10458                return Err(self.err(format!(
10459                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10460                    self.peek()
10461                )));
10462            }
10463            self.advance();
10464            self.expect_keyword_ident("encoding")?;
10465            if !matches!(self.peek(), Token::Eq) {
10466                return Err(self.err(format!(
10467                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10468                    self.peek()
10469                )));
10470            }
10471            self.advance();
10472            let enc_ident = match self.advance() {
10473                Token::Ident(s) | Token::QuotedIdent(s) => s,
10474                other => {
10475                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10476                }
10477            };
10478            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10479                "f32" => VecEncoding::F32,
10480                "sq8" => VecEncoding::Sq8,
10481                "half" => VecEncoding::F16,
10482                other => {
10483                    return Err(self.err(format!(
10484                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10485                    )));
10486                }
10487            };
10488            if !matches!(self.peek(), Token::RParen) {
10489                return Err(self.err(format!(
10490                    "expected ')' after encoding value, got {:?}",
10491                    self.peek()
10492                )));
10493            }
10494            self.advance();
10495            Some(enc)
10496        } else {
10497            None
10498        };
10499        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10500            name,
10501            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10502        }))
10503    }
10504
10505    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10506    /// only `SET` form currently supported; future v6.7.x can add
10507    /// more SET subjects without changing the dispatch shape.
10508    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10509    /// subactions. Single-subaction shape stays a 1-element vec.
10510    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10511        let table_name = self.expect_ident_like()?;
10512        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10513        loop {
10514            let subaction = self.parse_alter_table_subaction()?;
10515            // ADD COLUMN with inline REFERENCES emits both an
10516            // AddColumn and an AddForeignKey subaction; the
10517            // helper returns 1 or 2 items.
10518            targets.extend(subaction);
10519            if matches!(self.peek(), Token::Comma) {
10520                self.advance();
10521                continue;
10522            }
10523            break;
10524        }
10525        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10526            name: table_name,
10527            targets,
10528        }))
10529    }
10530
10531    /// Parse one ALTER TABLE subaction. Returns a Vec because
10532    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10533    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10534    fn parse_alter_table_subaction(
10535        &mut self,
10536    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10537        match self.peek() {
10538            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10539                self.advance();
10540                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10541                // storage parameters: paren-prefixed; consume.
10542                if matches!(self.peek(), Token::LParen) {
10543                    self.consume_until_statement_boundary();
10544                    return Ok(Vec::new());
10545                }
10546                let setting = self.expect_ident_like()?;
10547                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10548                    if !matches!(self.peek(), Token::Eq) {
10549                        return Err(self.err(alloc::format!(
10550                            "expected '=' after hot_tier_bytes, got {:?}",
10551                            self.peek()
10552                        )));
10553                    }
10554                    self.advance();
10555                    let n = self.expect_u64_literal()?;
10556                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10557                }
10558                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10559                // accept-and-no-op for ALTER TABLE SET <subject>
10560                // forms that pg_dump emits but SPG either treats
10561                // as N/A (single-tenant, single-owner, no shared
10562                // tablespaces) or accepts the dump-side declaration
10563                // without runtime effect:
10564                //   SET SCHEMA <name>            (18.11)
10565                //   SET TABLESPACE <name>        (18.8)
10566                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10567                //   SET WITHOUT CLUSTER          (18.13)
10568                //   SET WITHOUT OIDS             (PG legacy)
10569                //   SET (option = value, …)      (storage parameters)
10570                //   SET REPLICA IDENTITY {…}     (18.14)
10571                if setting.eq_ignore_ascii_case("schema")
10572                    || setting.eq_ignore_ascii_case("tablespace")
10573                    || setting.eq_ignore_ascii_case("logged")
10574                    || setting.eq_ignore_ascii_case("unlogged")
10575                    || setting.eq_ignore_ascii_case("without")
10576                {
10577                    self.consume_until_statement_boundary();
10578                    return Ok(Vec::new());
10579                }
10580                if setting.eq_ignore_ascii_case("replica") {
10581                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10582                    self.consume_until_statement_boundary();
10583                    return Ok(Vec::new());
10584                }
10585                // SET (option=value, …) — storage parameters.
10586                if matches!(self.peek(), Token::LParen) {
10587                    self.consume_until_statement_boundary();
10588                    return Ok(Vec::new());
10589                }
10590                Err(self.err(alloc::format!(
10591                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10592                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10593                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10594                )))
10595            }
10596            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10597            // not ignored: round 645 gave SPG the inheritance the
10598            // v7.37.18 no-op said it did not have.
10599            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10600                self.advance();
10601                let parent = self.expect_ident_like()?;
10602                self.consume_until_statement_boundary();
10603                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10604                    parent,
10605                    detach: false
10606                }])
10607            }
10608            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10609            // LEVEL SECURITY`, which has its own RLS arm below — without
10610            // the guard this swallowed NO FORCE as a no-op.
10611            Token::Ident(s)
10612                if s.eq_ignore_ascii_case("no")
10613                    && !matches!(
10614                        self.tokens.get(self.pos + 1),
10615                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10616                    ) =>
10617            {
10618                self.advance();
10619                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10620                    if k.eq_ignore_ascii_case("inherit"))
10621                {
10622                    self.advance();
10623                    let parent = self.expect_ident_like()?;
10624                    self.consume_until_statement_boundary();
10625                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10626                        parent,
10627                        detach: true
10628                    }]);
10629                }
10630                self.consume_until_statement_boundary();
10631                Ok(Vec::new())
10632            }
10633            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10634            // single-owner, so there is still nothing to record.
10635            //
10636            // v7.39 (round 652) — but the name now reaches the engine,
10637            // which refuses a role that does not exist as PG does. The
10638            // no-op was swallowing the whole statement, so a dump naming
10639            // a role this server never heard of restored clean and left
10640            // the table owned by whoever ran the restore.
10641            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10642                self.advance();
10643                if matches!(self.peek(), Token::To) {
10644                    self.advance();
10645                }
10646                let role = self.expect_ident_like()?;
10647                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10648                    role
10649                }])
10650            }
10651            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10652            // PG sets a hint; SPG doesn't have clustered storage, so the
10653            // hint itself stays a no-op.
10654            //
10655            // v7.39 (round 652) — the index name is checked now. PG
10656            // errors on one that does not exist, and swallowing that let
10657            // a typo'd CLUSTER ON pass silently.
10658            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10659                self.advance();
10660                // `ON` is a reserved token, not an ident.
10661                if !matches!(self.peek(), Token::On) {
10662                    return Err(self.err(alloc::format!(
10663                        "expected ON after CLUSTER, got {:?}",
10664                        self.peek()
10665                    )));
10666                }
10667                self.advance();
10668                let index = self.expect_ident_like()?;
10669                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10670                    index: Some(index)
10671                }])
10672            }
10673            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10674            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10675            // what a logical decoder puts in the old-tuple image; SPG's
10676            // replication is SQL-text, so there is nothing to record.
10677            // Accept-and-no-op (it used to be a parse error).
10678            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10679                self.advance();
10680                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10681                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10682                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10683                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10684                {
10685                    self.advance(); // IDENTITY
10686                    self.advance(); // USING
10687                    if matches!(self.peek(), Token::Index)
10688                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10689                    {
10690                        self.advance();
10691                    }
10692                    let index = self.expect_ident_like()?;
10693                    self.consume_until_statement_boundary();
10694                    return Ok(alloc::vec![
10695                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10696                    ]);
10697                }
10698                self.consume_until_statement_boundary();
10699                Ok(Vec::new())
10700            }
10701            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10702            //
10703            // v7.39 (round 652) — it used to consume the statement and
10704            // return nothing, on the stated theory that SPG validated at
10705            // ADD CONSTRAINT time so there was never anything left to
10706            // validate. Measured against PG18, ADD CONSTRAINT did not
10707            // scan the existing rows at all — the comment described a
10708            // property SPG did not have, which is why nobody looked. Both
10709            // halves are real now: ADD scans unless told NOT VALID, and
10710            // this scans what NOT VALID skipped.
10711            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10712                self.advance();
10713                self.expect_keyword_ident("constraint")?;
10714                let name = self.expect_ident_like()?;
10715                Ok(alloc::vec![
10716                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10717                ])
10718            }
10719            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10720            // SET (option = value, …). PG uses it to clear per-table
10721            // storage params like fillfactor or autovacuum_*. SPG
10722            // engine-manages those parameters; accept-and-no-op.
10723            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10724                self.advance();
10725                self.consume_until_statement_boundary();
10726                Ok(Vec::new())
10727            }
10728            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10729            // type-of binding (PG 9.0+). SPG composite types
10730            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10731            // TABLE OF is rare and inverse of CREATE TABLE OF.
10732            // Accept-and-no-op until a customer dump round-trips it.
10733            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10734                self.advance();
10735                // v7.39 (round 710) — the type name is validated now.
10736                let type_name = self.expect_ident_like()?;
10737                self.consume_until_statement_boundary();
10738                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10739                    type_name
10740                }])
10741            }
10742            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10743            // (reserved keyword) rather than Token::Ident("not"),
10744            // so it needs its own arm. Accept-and-no-op same as OF.
10745            Token::Not => {
10746                self.advance();
10747                self.consume_until_statement_boundary();
10748                Ok(Vec::new())
10749            }
10750            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10751            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10752                self.advance();
10753                self.expect_row_level_security()?;
10754                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10755                    enabled: None,
10756                    force: Some(true),
10757                }])
10758            }
10759            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10760            Token::Ident(s)
10761                if s.eq_ignore_ascii_case("no")
10762                    && matches!(
10763                        self.tokens.get(self.pos + 1),
10764                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10765                    ) =>
10766            {
10767                self.advance(); // NO
10768                self.advance(); // FORCE
10769                self.expect_row_level_security()?;
10770                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10771                    enabled: None,
10772                    force: Some(false),
10773                }])
10774            }
10775            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10776            // (sets relrowsecurity). The guard requires the next token to be
10777            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10778            Token::Ident(s)
10779                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10780                    && matches!(
10781                        self.tokens.get(self.pos + 1),
10782                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10783                    ) =>
10784            {
10785                let enabled = s.eq_ignore_ascii_case("enable");
10786                self.advance(); // ENABLE/DISABLE
10787                self.expect_row_level_security()?;
10788                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10789                    enabled: Some(enabled),
10790                    force: None,
10791                }])
10792            }
10793            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10794                self.advance();
10795                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10796                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10797                // emits. The same grammar CREATE TABLE already accepts
10798                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10799                // through the SAME parser — an ALTER-only copy would be a
10800                // second place for the two to drift.
10801                if self.peek_mysql_inline_key_start() {
10802                    return Ok(match self.parse_mysql_inline_key()? {
10803                        Some(c) => {
10804                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10805                        }
10806                        // FULLTEXT / SPATIAL parse and are accepted as a
10807                        // no-op here exactly as they are inline.
10808                        None => Vec::new(),
10809                    });
10810                }
10811                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10812                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10813                // PRIMARY KEY this way; mysqldump emits both.
10814                // Peek-only dispatch (no advance) — `advance()`
10815                // destructively replaces consumed tokens with Eof,
10816                // so saved-pos restore would land on Eofs.
10817                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10818                {
10819                    // The next-but-one ident is the constraint
10820                    // name; the one after THAT is the kind.
10821                    let kind_pos = self.pos + 2;
10822                    let kind = self.tokens.get(kind_pos).cloned();
10823                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10824                    {
10825                        let fk = self.parse_table_level_fk()?;
10826                        return Ok(alloc::vec![
10827                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10828                        ]);
10829                    }
10830                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10831                    {
10832                        self.advance(); // CONSTRAINT
10833                        // v7.39 (read01 round 48) — keep the name; the engine
10834                        // stores it now instead of dropping it on the floor.
10835                        let con_name = self.expect_ident_like()?;
10836                        self.advance(); // PRIMARY
10837                        self.expect_keyword_ident("key")?;
10838                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10839                        // v7.39 (round 711) — the ALTER form carries the
10840                        // timing too (pg_dump writes it here).
10841                        let (deferrable, initially_deferred) =
10842                            self.consume_deferrable_clauses_timed()?;
10843                        return Ok(alloc::vec![
10844                            crate::ast::AlterTableTarget::AddTableConstraint(
10845                                crate::ast::TableConstraint::PrimaryKey {
10846                                    name: Some(con_name),
10847                                    columns: cols,
10848                                    deferrable,
10849                                    initially_deferred,
10850                                }
10851                            )
10852                        ]);
10853                    }
10854                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10855                    {
10856                        self.advance(); // CONSTRAINT
10857                        // v7.39 (read01 round 48) — keep the name.
10858                        let con_name = self.expect_ident_like()?;
10859                        // v7.22 (mailrs round-13 gap 6) — delegate so
10860                        // the optional `NULLS [NOT] DISTINCT` modifier
10861                        // parses here too (pg_dump emits the ALTER
10862                        // form; semantics enforced by the engine
10863                        // since v7.13).
10864                        let mut uc = self.parse_table_level_unique()?;
10865                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10866                            *name = Some(con_name);
10867                        }
10868                        return Ok(alloc::vec![
10869                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10870                        ]);
10871                    }
10872                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10873                    {
10874                        self.advance(); // CONSTRAINT
10875                        // v7.39 (read01 round 48) — keep the name.
10876                        let con_name = self.expect_ident_like()?;
10877                        self.advance(); // CHECK
10878                        if !matches!(self.peek(), Token::LParen) {
10879                            return Err(self.err(alloc::format!(
10880                                "expected '(' after CHECK, got {:?}", self.peek()
10881                            )));
10882                        }
10883                        self.advance();
10884                        let expr = self.parse_expr(0)?;
10885                        if matches!(self.peek(), Token::RParen) {
10886                            self.advance();
10887                        }
10888                        let not_valid = self.parse_not_valid_suffix();
10889                        return Ok(alloc::vec![
10890                            crate::ast::AlterTableTarget::AddTableConstraint(
10891                                crate::ast::TableConstraint::Check {
10892                                    name: Some(con_name),
10893                                    expr,
10894                                    not_valid,
10895                                }
10896                            )
10897                        ]);
10898                    }
10899                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10900                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10901                    // exclusion constraints via this ALTER form.
10902                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10903                    {
10904                        self.advance(); // CONSTRAINT
10905                        let con_name = self.expect_ident_like()?;
10906                        let mut ex = self.parse_table_level_exclude()?;
10907                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10908                            *name = Some(con_name);
10909                        }
10910                        return Ok(alloc::vec![
10911                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10912                        ]);
10913                    }
10914                    // Unknown kind — fall through to FK path which
10915                    // produces a descriptive parse error.
10916                }
10917                let is_fk = matches!(
10918                    self.peek(),
10919                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10920                        || s.eq_ignore_ascii_case("foreign")
10921                );
10922                if is_fk {
10923                    let fk = self.parse_table_level_fk()?;
10924                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10925                }
10926                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10927                // (no CONSTRAINT prefix) — same dispatch.
10928                match self.peek().clone() {
10929                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10930                        self.advance();
10931                        self.expect_keyword_ident("key")?;
10932                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10933                        let (deferrable, initially_deferred) =
10934                            self.consume_deferrable_clauses_timed()?;
10935                        return Ok(alloc::vec![
10936                            crate::ast::AlterTableTarget::AddTableConstraint(
10937                                crate::ast::TableConstraint::PrimaryKey {
10938                                    name: None,
10939                                    columns: cols,
10940                                    deferrable,
10941                                    initially_deferred,
10942                                }
10943                            )
10944                        ]);
10945                    }
10946                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10947                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
10948                        let uc = self.parse_table_level_unique()?;
10949                        return Ok(alloc::vec![
10950                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
10951                        ]);
10952                    }
10953                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10954                    // prefix). The other three bare forms were here and
10955                    // this one was not, so it fell through to the column
10956                    // path and came back as "unexpected reserved keyword
10957                    // 'check' at start of column definition".
10958                    _ if self.peek_table_level_check_start() => {
10959                        let chk = self.parse_table_level_check()?;
10960                        let not_valid = self.parse_not_valid_suffix();
10961                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10962                            unreachable!("parse_table_level_check returns Check")
10963                        };
10964                        return Ok(alloc::vec![
10965                            crate::ast::AlterTableTarget::AddTableConstraint(
10966                                crate::ast::TableConstraint::Check {
10967                                    name: None,
10968                                    expr,
10969                                    not_valid,
10970                                }
10971                            )
10972                        ]);
10973                    }
10974                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10975                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10976                        let ex = self.parse_table_level_exclude()?;
10977                        return Ok(alloc::vec![
10978                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
10979                        ]);
10980                    }
10981                    _ => {}
10982                }
10983                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10984                    self.advance();
10985                }
10986                let mut if_not_exists = false;
10987                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10988                    self.advance();
10989                    if !matches!(self.peek(), Token::Not) {
10990                        return Err(self.err(alloc::format!(
10991                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10992                            self.peek()
10993                        )));
10994                    }
10995                    self.advance();
10996                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10997                        return Err(self.err(alloc::format!(
10998                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10999                            self.peek()
11000                        )));
11001                    }
11002                    self.advance();
11003                    if_not_exists = true;
11004                }
11005                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11006                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11007                // returns ColumnDef + an optional inline FK.
11008                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11009                let col_name = column.name.clone();
11010                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11011                    column,
11012                    if_not_exists,
11013                }];
11014                if let Some(mut fk) = col_level_fk {
11015                    if fk.columns.is_empty() {
11016                        fk.columns.push(col_name);
11017                    }
11018                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11019                }
11020                Ok(out)
11021            }
11022            Token::Drop => {
11023                self.advance();
11024                // v7.13.3 — dispatch on the next token. mailrs round-7
11025                // S8 closed DROP COLUMN; round-6 S7 closed
11026                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11027                // RESTRICT modifiers.
11028                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11029                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11030                let subject = match self.peek() {
11031                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11032                        self.advance();
11033                        "constraint"
11034                    }
11035                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11036                        self.advance();
11037                        "column"
11038                    }
11039                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11040                    // `INDEX` lexes as the reserved Token::Index, so it is
11041                    // unambiguous. `KEY` is a plain ident, and PG allows a
11042                    // column literally named "key", so only read it as the
11043                    // keyword when a name follows it.
11044                    Token::Index => {
11045                        self.advance();
11046                        "index"
11047                    }
11048                    Token::Ident(s)
11049                        if s.eq_ignore_ascii_case("key")
11050                            && matches!(
11051                                self.tokens.get(self.pos + 1),
11052                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11053                            ) =>
11054                    {
11055                        self.advance();
11056                        "index"
11057                    }
11058                    // PG-canonical bare `DROP <col>` without COLUMN
11059                    // keyword is also valid; treat any other ident
11060                    // as the column name.
11061                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11062                    other => {
11063                        return Err(self.err(alloc::format!(
11064                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11065                        )));
11066                    }
11067                };
11068                let mut if_exists = false;
11069                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11070                    let n1 = self.tokens.get(self.pos + 1);
11071                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11072                        self.advance();
11073                        self.advance();
11074                        if_exists = true;
11075                    }
11076                }
11077                let name = self.expect_ident_like()?;
11078                let mut cascade = false;
11079                if matches!(
11080                    self.peek(),
11081                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11082                        || s.eq_ignore_ascii_case("restrict")
11083                ) {
11084                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11085                    {
11086                        cascade = true;
11087                    }
11088                    self.advance();
11089                }
11090                if subject == "index" {
11091                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11092                        name,
11093                        if_exists,
11094                    }])
11095                } else if subject == "constraint" {
11096                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11097                        name,
11098                        if_exists,
11099                    }])
11100                } else {
11101                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11102                        column: name,
11103                        if_exists,
11104                        cascade,
11105                    }])
11106                }
11107            }
11108            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11109                self.advance();
11110                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11111                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11112                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11113                // immediately; accept-and-no-op.
11114                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11115                    self.advance();
11116                    self.consume_until_statement_boundary();
11117                    return Ok(Vec::new());
11118                }
11119                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11120                    self.advance();
11121                }
11122                let col_name = self.expect_ident_like()?;
11123                match self.peek() {
11124                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11125                        self.advance();
11126                    }
11127                    // v7.14.0 — pg_dump emits BIGSERIAL via
11128                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11129                    // nextval('seq')` (the sequence is created
11130                    // separately). SPG's BIGSERIAL already uses
11131                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11132                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11133                    // engine no-ops by consuming the tail.
11134                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11135                        // v7.22 (round-13 T2) — `SET DEFAULT
11136                        // nextval('…')` is how pg_dump spells a
11137                        // SERIAL column (plain integer in CREATE
11138                        // TABLE + this ALTER). It used to be
11139                        // swallowed as a no-op, which silently
11140                        // STRIPPED auto-increment from imported
11141                        // schemas — the first post-import INSERT
11142                        // without an explicit id then violated NOT
11143                        // NULL. Lower it to the auto-increment
11144                        // marker instead.
11145                        let is_default_nextval =
11146                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11147                                && matches!(
11148                                    self.tokens.get(self.pos + 2),
11149                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11150                                );
11151                        if is_default_nextval {
11152                            let seq_name = self.scan_sequence_name_until_boundary();
11153                            return Ok(alloc::vec![
11154                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11155                                    column: col_name,
11156                                    seq_name,
11157                                }
11158                            ]);
11159                        }
11160                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11161                        self.advance(); // consume "set"
11162                        match self.peek().clone() {
11163                            Token::Default => {
11164                                self.advance();
11165                                let default_expr = self.parse_expr(0)?;
11166                                return Ok(alloc::vec![
11167                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11168                                        column: col_name,
11169                                        default_expr,
11170                                    }
11171                                ]);
11172                            }
11173                            Token::Not => {
11174                                self.advance();
11175                                if !matches!(self.peek(), Token::Null) {
11176                                    return Err(self.err(alloc::format!(
11177                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11178                                        self.peek()
11179                                    )));
11180                                }
11181                                self.advance();
11182                                return Ok(alloc::vec![
11183                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11184                                        column: col_name,
11185                                    }
11186                                ]);
11187                            }
11188                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11189                            // stored generated column's expression and
11190                            // recompute existing rows.
11191                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11192                                self.advance(); // EXPRESSION
11193                                if matches!(self.peek(), Token::As) {
11194                                    self.advance();
11195                                }
11196                                let expr = self.parse_expr(0)?;
11197                                return Ok(alloc::vec![
11198                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11199                                        column: col_name,
11200                                        expr,
11201                                    }
11202                                ]);
11203                            }
11204                            other => {
11205                                // Other SET subjects (STATISTICS,
11206                                // STORAGE, COMPRESSION, …) stay no-ops —
11207                                // storage hints with no SPG semantics.
11208                                let _ = other;
11209                                self.consume_until_statement_boundary();
11210                                return Ok(Vec::new());
11211                            }
11212                        }
11213                    }
11214                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11215                        self.advance(); // consume "drop"
11216                        return self.parse_alter_column_drop_tail(col_name);
11217                    }
11218                    Token::Drop => {
11219                        self.advance(); // consume Drop token
11220                        return self.parse_alter_column_drop_tail(col_name);
11221                    }
11222                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11223                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11224                        // GENERATED { ALWAYS | BY DEFAULT } AS
11225                        // IDENTITY ( … )`: pg_dump's spelling for
11226                        // identity columns. Same auto-increment
11227                        // lowering as the nextval default; the
11228                        // sequence options inside the parens are
11229                        // no-ops under SPG's max+1 semantics.
11230                        let is_generated = matches!(
11231                            self.tokens.get(self.pos + 1),
11232                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11233                        );
11234                        if !is_generated {
11235                            return Err(self.err(alloc::format!(
11236                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11237                                self.tokens.get(self.pos + 1)
11238                            )));
11239                        }
11240                        let seq_name = self.scan_sequence_name_until_boundary();
11241                        return Ok(alloc::vec![
11242                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11243                                column: col_name,
11244                                seq_name,
11245                            }
11246                        ]);
11247                    }
11248                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11249                    // column: floor the next allocated value at n (bare
11250                    // RESTART = restart from the start value, 1).
11251                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11252                        self.advance();
11253                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11254                        {
11255                            self.advance();
11256                            let neg = if matches!(self.peek(), Token::Minus) {
11257                                self.advance();
11258                                true
11259                            } else {
11260                                false
11261                            };
11262                            match self.advance() {
11263                                Token::Integer(v) => Some(if neg { -v } else { v }),
11264                                other => {
11265                                    return Err(self.err(alloc::format!(
11266                                        "expected integer after RESTART WITH, got {other:?}"
11267                                    )));
11268                                }
11269                            }
11270                        } else {
11271                            None
11272                        };
11273                        return Ok(alloc::vec![
11274                            crate::ast::AlterTableTarget::AlterColumnRestart {
11275                                column: col_name,
11276                                with,
11277                            }
11278                        ]);
11279                    }
11280                    other => {
11281                        return Err(self.err(alloc::format!(
11282                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11283                        )));
11284                    }
11285                }
11286                // v7.39 (round 713) — the type parser has consumed a
11287                // trailing `COLLATE <name>` since Phase 2.5, and
11288                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11289                // TYPE text COLLATE "C"` parsed clean and changed
11290                // nothing. Keep the clause; the engine re-collates.
11291                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11292                    self.parse_type_with_implied_flags()?;
11293                let collation = if coll_explicit {
11294                    coll_name.map(|n| (coll, n))
11295                } else {
11296                    None
11297                };
11298                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11299                {
11300                    self.advance();
11301                    Some(self.parse_expr(0)?)
11302                } else {
11303                    None
11304                };
11305                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11306                    column: col_name,
11307                    new_type,
11308                    using,
11309                    collation,
11310                }])
11311            }
11312            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11313            // PG also supports `RENAME TO new_table` for table-name
11314            // rename; that surface is deferred (pg_dump never emits
11315            // it). If the first post-RENAME ident is `TO`, the user
11316            // is asking for table rename — error with a clear
11317            // message rather than misparsing `TO` as a column name.
11318            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11319                self.advance();
11320                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11321                // table-name rename (mailrs round-10 A.5 — used
11322                // by migrate-042's `RENAME TO email_contacts`).
11323                // `TO` lexes as Token::To.
11324                if matches!(self.peek(), Token::To)
11325                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11326                {
11327                    self.advance();
11328                    let new = self.expect_ident_like()?;
11329                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11330                        new,
11331                    }]);
11332                }
11333                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11334                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11335                    self.advance();
11336                    let old = self.expect_ident_like()?;
11337                    if matches!(self.peek(), Token::To) {
11338                        self.advance();
11339                    } else {
11340                        self.expect_keyword_ident("to")?;
11341                    }
11342                    let new = self.expect_ident_like()?;
11343                    return Ok(alloc::vec![
11344                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11345                    ]);
11346                }
11347                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11348                    self.advance();
11349                }
11350                let old = self.expect_ident_like()?;
11351                // `TO` is a reserved keyword token; accept both
11352                // Token::To and Token::Ident("to") for consistency.
11353                if matches!(self.peek(), Token::To) {
11354                    self.advance();
11355                } else {
11356                    self.expect_keyword_ident("to")?;
11357                }
11358                let new = self.expect_ident_like()?;
11359                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11360                    old,
11361                    new,
11362                }])
11363            }
11364            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11365            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11366            // every data block with these. Real disable semantics —
11367            // not no-op — because reload correctness assumes the
11368            // triggers don't fire (rows already carry their
11369            // computed values from prod).
11370            Token::Ident(s)
11371                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11372            {
11373                let enabled = s.eq_ignore_ascii_case("enable");
11374                self.advance();
11375                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11376                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11377                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11378                // pg_dump output) — anything else falls through to
11379                // the catch-all error below.
11380                // v7.22 (round-13 T3) — mysqldump wraps every data
11381                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11382                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11383                // maintains indexes incrementally — engine no-op.
11384                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11385                    self.advance();
11386                    return Ok(Vec::new());
11387                }
11388                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11389                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11390                // to gate triggers on session_replication_role; SPG
11391                // has no replica role, so the prefix is consumed and
11392                // treated identically to the plain ENABLE/DISABLE
11393                // TRIGGER form.
11394                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11395                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11396                {
11397                    self.advance();
11398                }
11399                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11400                    return Err(self.err(alloc::format!(
11401                        "expected TRIGGER after {}, got {:?}",
11402                        if enabled { "ENABLE" } else { "DISABLE" },
11403                        self.peek()
11404                    )));
11405                }
11406                self.advance();
11407                // `ALL` lexes as Token::All (reserved); also
11408                // accept Token::Ident("all") for symmetry.
11409                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11410                // TRIGGER selectors. USER (= all user triggers) is
11411                // semantically ALL here; REPLICA / ALWAYS gate on
11412                // session_replication_role which SPG doesn't track.
11413                // All map to TriggerSelector::All.
11414                let which = if matches!(self.peek(), Token::All)
11415                    || matches!(self.peek(), Token::Ident(s)
11416                        if s.eq_ignore_ascii_case("all")
11417                            || s.eq_ignore_ascii_case("user")
11418                            || s.eq_ignore_ascii_case("replica")
11419                            || s.eq_ignore_ascii_case("always"))
11420                {
11421                    self.advance();
11422                    crate::ast::TriggerSelector::All
11423                } else {
11424                    let name = self.expect_ident_like()?;
11425                    crate::ast::TriggerSelector::Named(name)
11426                };
11427                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11428                    which,
11429                    enabled,
11430                }])
11431            }
11432            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11433            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11434                self.advance();
11435                if !matches!(self.peek(), Token::Partition)
11436                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11437                        if s.eq_ignore_ascii_case("partition"))
11438                {
11439                    return Err(self.err(alloc::format!(
11440                        "expected PARTITION after ATTACH, got {:?}",
11441                        self.peek()
11442                    )));
11443                }
11444                self.advance();
11445                let child = self.expect_ident_like()?;
11446                let bounds = self.parse_partition_bounds_tail()?;
11447                Ok(alloc::vec![
11448                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11449                ])
11450            }
11451            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11452            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11453                self.advance();
11454                if !matches!(self.peek(), Token::Partition)
11455                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11456                        if s.eq_ignore_ascii_case("partition"))
11457                {
11458                    return Err(self.err(alloc::format!(
11459                        "expected PARTITION after DETACH, got {:?}",
11460                        self.peek()
11461                    )));
11462                }
11463                self.advance();
11464                let child = self.expect_ident_like()?;
11465                let mut concurrently = false;
11466                let mut finalize = false;
11467                loop {
11468                    match self.peek().clone() {
11469                        Token::Ident(s) | Token::QuotedIdent(s)
11470                            if s.eq_ignore_ascii_case("concurrently") =>
11471                        {
11472                            self.advance();
11473                            concurrently = true;
11474                        }
11475                        Token::Ident(s) | Token::QuotedIdent(s)
11476                            if s.eq_ignore_ascii_case("finalize") =>
11477                        {
11478                            self.advance();
11479                            finalize = true;
11480                        }
11481                        _ => break,
11482                    }
11483                }
11484                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11485                    child,
11486                    concurrently,
11487                    finalize,
11488                }])
11489            }
11490            other => Err(self.err(alloc::format!(
11491                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11492            ))),
11493        }
11494    }
11495
11496    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11497    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11498    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11499    /// `parse_partition_of_tail`'s bounds branch.
11500    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11501    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11502    /// lowering each to the respective AlterTableTarget. Any
11503    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11504    /// no-op via consume_until_statement_boundary.
11505    fn parse_alter_column_drop_tail(
11506        &mut self,
11507        col_name: String,
11508    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11509        match self.peek().clone() {
11510            Token::Default => {
11511                self.advance();
11512                Ok(alloc::vec![
11513                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11514                ])
11515            }
11516            Token::Not => {
11517                self.advance();
11518                if !matches!(self.peek(), Token::Null) {
11519                    return Err(self.err(alloc::format!(
11520                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11521                        self.peek()
11522                    )));
11523                }
11524                self.advance();
11525                Ok(alloc::vec![
11526                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11527                ])
11528            }
11529            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11530            // generated column into a plain column.
11531            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11532                self.advance();
11533                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11534                // dropped, so the engine still errored on a plain
11535                // column; PG's semantics are NOTICE + skip.
11536                let mut if_exists = false;
11537                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11538                    self.advance();
11539                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11540                        self.advance();
11541                        if_exists = true;
11542                    }
11543                }
11544                Ok(alloc::vec![
11545                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11546                        column: col_name,
11547                        if_exists,
11548                    }
11549                ])
11550            }
11551            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11552            // identity column into a plain column.
11553            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11554                self.advance();
11555                let mut if_exists = false;
11556                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11557                    self.advance();
11558                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11559                        self.advance();
11560                        if_exists = true;
11561                    }
11562                }
11563                Ok(alloc::vec![
11564                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11565                        column: col_name,
11566                        if_exists,
11567                    }
11568                ])
11569            }
11570            _ => {
11571                self.consume_until_statement_boundary();
11572                Ok(Vec::new())
11573            }
11574        }
11575    }
11576
11577    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11578    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11579    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11580    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11581    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11582        let mut opts = crate::ast::CopyOptions::default();
11583        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11584            return Ok(opts);
11585        }
11586        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11587            self.advance();
11588        }
11589        if matches!(self.peek(), Token::LParen) {
11590            self.advance();
11591            loop {
11592                self.parse_one_copy_option(&mut opts)?;
11593                match self.peek() {
11594                    Token::Comma => {
11595                        self.advance();
11596                    }
11597                    Token::RParen => {
11598                        self.advance();
11599                        break;
11600                    }
11601                    other => {
11602                        return Err(self.err(alloc::format!(
11603                            "expected ',' or ')' in COPY options, got {other:?}"
11604                        )));
11605                    }
11606                }
11607            }
11608        } else {
11609            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11610                self.parse_one_copy_option(&mut opts)?;
11611            }
11612        }
11613        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11614            return Err(self.err(alloc::format!(
11615                "unexpected token after COPY options: {:?}",
11616                self.peek()
11617            )));
11618        }
11619        Ok(opts)
11620    }
11621
11622    fn parse_one_copy_option(
11623        &mut self,
11624        opts: &mut crate::ast::CopyOptions,
11625    ) -> Result<(), ParseError> {
11626        use crate::ast::CopyFormat;
11627        // The option keyword. NULL lexes as its own token; the rest are
11628        // bare identifiers.
11629        let kw = match self.advance() {
11630            Token::Null => alloc::string::String::from("NULL"),
11631            Token::Ident(s) => s.to_uppercase(),
11632            other => {
11633                return Err(self.err(alloc::format!(
11634                    "expected a COPY option keyword, got {other:?}"
11635                )));
11636            }
11637        };
11638        match kw.as_str() {
11639            "FORMAT" => {
11640                let fmt = self.expect_ident_like()?;
11641                match fmt.to_ascii_uppercase().as_str() {
11642                    "CSV" => opts.format = CopyFormat::Csv,
11643                    "TEXT" => opts.format = CopyFormat::Text,
11644                    other => {
11645                        return Err(self.err(alloc::format!(
11646                            "COPY format \"{}\" not recognized",
11647                            other.to_ascii_lowercase()
11648                        )));
11649                    }
11650                }
11651            }
11652            // Legacy bare format keywords.
11653            "CSV" => opts.format = CopyFormat::Csv,
11654            "TEXT" => opts.format = CopyFormat::Text,
11655            "HEADER" => {
11656                opts.header = match self.peek() {
11657                    Token::True => {
11658                        self.advance();
11659                        true
11660                    }
11661                    Token::False => {
11662                        self.advance();
11663                        false
11664                    }
11665                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11666                        self.advance();
11667                        true
11668                    }
11669                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11670                        self.advance();
11671                        false
11672                    }
11673                    // Bare HEADER (no boolean) means HEADER true.
11674                    _ => true,
11675                };
11676            }
11677            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11678                let s = match self.advance() {
11679                    Token::String(s) => s,
11680                    other => {
11681                        return Err(self.err(alloc::format!(
11682                            "COPY {kw} expects a single-character string, got {other:?}"
11683                        )));
11684                    }
11685                };
11686                // v7.39 (round 247) — PG's wording (0A000), keyword in
11687                // lowercase: "COPY delimiter must be a single one-byte
11688                // character".
11689                let one_byte_err = || {
11690                    self.err(alloc::format!(
11691                        "COPY {} must be a single one-byte character",
11692                        kw.to_ascii_lowercase()
11693                    ))
11694                };
11695                let mut chars = s.chars();
11696                let c = chars.next().ok_or_else(one_byte_err)?;
11697                if chars.next().is_some() || c.len_utf8() != 1 {
11698                    return Err(one_byte_err());
11699                }
11700                match kw.as_str() {
11701                    "DELIMITER" => opts.delimiter = Some(c),
11702                    "QUOTE" => opts.quote = Some(c),
11703                    _ => opts.escape = Some(c),
11704                }
11705            }
11706            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11707            "FORCE_QUOTE" => {
11708                if matches!(self.peek(), Token::Star) {
11709                    self.advance();
11710                    opts.force_quote = Some(Vec::new());
11711                } else {
11712                    if !matches!(self.peek(), Token::LParen) {
11713                        return Err(self.err(alloc::format!(
11714                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11715                            self.peek()
11716                        )));
11717                    }
11718                    self.advance();
11719                    let mut cols = Vec::new();
11720                    loop {
11721                        cols.push(self.expect_ident_like()?);
11722                        match self.peek() {
11723                            Token::Comma => {
11724                                self.advance();
11725                            }
11726                            Token::RParen => {
11727                                self.advance();
11728                                break;
11729                            }
11730                            other => {
11731                                return Err(self.err(alloc::format!(
11732                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11733                                )));
11734                            }
11735                        }
11736                    }
11737                    opts.force_quote = Some(cols);
11738                }
11739            }
11740            "NULL" => {
11741                opts.null_str = Some(match self.advance() {
11742                    Token::String(s) => s,
11743                    other => {
11744                        return Err(self.err(alloc::format!(
11745                            "COPY NULL expects a quoted string, got {other:?}"
11746                        )));
11747                    }
11748                });
11749            }
11750            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11751            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11752            // FORCE_NULL too.
11753            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11754                let cols = self.parse_copy_column_list(&kw)?;
11755                if kw == "FORCE_NOT_NULL" {
11756                    opts.force_not_null = Some(cols);
11757                } else {
11758                    opts.force_null = Some(cols);
11759                }
11760            }
11761            other => {
11762                // PG's wording, lowercased option name.
11763                return Err(self.err(alloc::format!(
11764                    "option \"{}\" not recognized",
11765                    other.to_ascii_lowercase()
11766                )));
11767            }
11768        }
11769        Ok(())
11770    }
11771
11772    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11773    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11774    /// is the `*` spelling.
11775    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11776        if matches!(self.peek(), Token::Star) {
11777            self.advance();
11778            return Ok(Vec::new());
11779        }
11780        if !matches!(self.peek(), Token::LParen) {
11781            return Err(self.err(alloc::format!(
11782                "expected '(' or '*' after {kw}, got {:?}",
11783                self.peek()
11784            )));
11785        }
11786        self.advance();
11787        let mut cols = Vec::new();
11788        loop {
11789            cols.push(self.expect_ident_like()?);
11790            match self.peek() {
11791                Token::Comma => {
11792                    self.advance();
11793                }
11794                Token::RParen => {
11795                    self.advance();
11796                    break;
11797                }
11798                other => {
11799                    return Err(self.err(alloc::format!(
11800                        "expected ',' or ')' in {kw} list, got {other:?}"
11801                    )));
11802                }
11803            }
11804        }
11805        Ok(cols)
11806    }
11807
11808    fn parse_partition_bounds_tail(
11809        &mut self,
11810    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11811        use crate::ast::PartitionOfBoundsAst;
11812        match self.peek() {
11813            Token::Default => {
11814                self.advance();
11815                Ok(PartitionOfBoundsAst::Default)
11816            }
11817            Token::For => {
11818                self.advance();
11819                if !matches!(self.peek(), Token::Values) {
11820                    return Err(
11821                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11822                    );
11823                }
11824                self.advance();
11825                let want_with = matches!(
11826                    self.peek(),
11827                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11828                );
11829                if want_with {
11830                    self.advance();
11831                    if !matches!(self.peek(), Token::LParen) {
11832                        return Err(self.err(format!(
11833                            "expected '(' after FOR VALUES WITH, got {:?}",
11834                            self.peek()
11835                        )));
11836                    }
11837                    self.advance();
11838                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11839                    loop {
11840                        let key = self.expect_ident_like()?;
11841                        let n = match self.peek().clone() {
11842                            Token::Integer(v) if u32::try_from(v).is_ok() => {
11843                                self.advance();
11844                                v as u32
11845                            }
11846                            other => {
11847                                return Err(self.err(format!(
11848                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11849                                )));
11850                            }
11851                        };
11852                        match key.to_ascii_uppercase().as_str() {
11853                            "MODULUS" => modulus = Some(n),
11854                            "REMAINDER" => remainder = Some(n),
11855                            other => {
11856                                return Err(self.err(format!(
11857                                    "FOR VALUES WITH: unknown key {other:?}; \
11858                                     expected MODULUS or REMAINDER"
11859                                )));
11860                            }
11861                        }
11862                        match self.peek() {
11863                            Token::Comma => {
11864                                self.advance();
11865                            }
11866                            Token::RParen => {
11867                                self.advance();
11868                                break;
11869                            }
11870                            other => {
11871                                return Err(self.err(format!(
11872                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11873                                )));
11874                            }
11875                        }
11876                    }
11877                    let modulus = modulus
11878                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11879                    let remainder = remainder.ok_or_else(|| {
11880                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11881                    })?;
11882                    if modulus == 0 {
11883                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11884                    }
11885                    if remainder >= modulus {
11886                        return Err(self.err(format!(
11887                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11888                        )));
11889                    }
11890                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11891                }
11892                match self.peek() {
11893                    Token::From => {
11894                        self.advance();
11895                        let lower = Box::new(self.parse_partition_bound_expr()?);
11896                        if !matches!(self.peek(), Token::To) {
11897                            return Err(self.err(format!(
11898                                "expected TO after FROM (...), got {:?}",
11899                                self.peek()
11900                            )));
11901                        }
11902                        self.advance();
11903                        let upper = Box::new(self.parse_partition_bound_expr()?);
11904                        Ok(PartitionOfBoundsAst::Range { lower, upper })
11905                    }
11906                    Token::In => {
11907                        self.advance();
11908                        if !matches!(self.peek(), Token::LParen) {
11909                            return Err(self.err(format!(
11910                                "expected '(' after FOR VALUES IN, got {:?}",
11911                                self.peek()
11912                            )));
11913                        }
11914                        self.advance();
11915                        let mut values = Vec::new();
11916                        loop {
11917                            values.push(self.parse_expr(0)?);
11918                            match self.peek() {
11919                                Token::Comma => {
11920                                    self.advance();
11921                                }
11922                                Token::RParen => {
11923                                    self.advance();
11924                                    break;
11925                                }
11926                                other => {
11927                                    return Err(self.err(format!(
11928                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11929                                    )));
11930                                }
11931                            }
11932                        }
11933                        if values.is_empty() {
11934                            return Err(
11935                                self.err("FOR VALUES IN requires at least one literal".to_string())
11936                            );
11937                        }
11938                        Ok(PartitionOfBoundsAst::List { values })
11939                    }
11940                    other => Err(self.err(format!(
11941                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11942                    ))),
11943                }
11944            }
11945            other => Err(self.err(format!(
11946                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11947            ))),
11948        }
11949    }
11950
11951    /// v7.16.2 — peek for `information_schema.<tbl>` /
11952    /// `pg_catalog.<tbl>` triples and, if matched, consume all
11953    /// three tokens + return a synthetic table name the engine's
11954    /// SELECT path recognises as a virtual view. Returns `None`
11955    /// when the head doesn't look like a meta-qualified name.
11956    /// Used by `parse_table_ref` to bypass the
11957    /// `expect_ident_like` schema-strip for these specific PG
11958    /// meta schemas (mailrs round-10 A.3).
11959    fn try_peek_meta_qualified(&mut self) -> Option<String> {
11960        // Extract the schema name. Must be a plain ident token.
11961        let schema = match self.tokens.get(self.pos) {
11962            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11963            _ => return None,
11964        };
11965        // Dot.
11966        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11967            return None;
11968        }
11969        // The table-side ident may lex as a reserved keyword
11970        // (e.g. `Token::Tables`). Tolerate the common ones via a
11971        // helper that reads the trailing token's underlying name.
11972        let tbl = match self.tokens.get(self.pos + 2)? {
11973            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11974            Token::Tables => "tables".to_string(),
11975            // Other PG meta table names that may collide with
11976            // reserved keywords land here as needed.
11977            _ => return None,
11978        };
11979        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11980        // names so the synthetic name doesn't double-prefix
11981        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11982        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11983            ("__spg_info_", tbl.to_ascii_lowercase())
11984        } else if schema.eq_ignore_ascii_case("pg_catalog") {
11985            // v7.39 (round 541) — only the catalogs SPG actually
11986            // synthesises are rewritten, which is what the BARE path
11987            // has always checked. Anything else keeps its own name and
11988            // takes the ordinary route: `pg_stat_activity` and friends
11989            // resolve through meta_view_result, and a name that is no
11990            // catalog at all gets PG's "relation does not exist"
11991            // instead of a message about a view SPG cannot materialise.
11992            let lowered = tbl.to_ascii_lowercase();
11993            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11994                self.advance(); // schema
11995                self.advance(); // dot
11996                self.advance(); // tbl
11997                return Some(lowered);
11998            }
11999            let bare = lowered
12000                .strip_prefix("pg_")
12001                .map(alloc::string::String::from)
12002                .unwrap_or(lowered);
12003            ("__spg_pg_", bare)
12004        } else if schema.eq_ignore_ascii_case("mysql") {
12005            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12006            // (`mysql.user`, `mysql.db`). Same synthetic-name
12007            // shape as pg_catalog.
12008            ("__spg_mysql_", tbl.to_ascii_lowercase())
12009        } else {
12010            return None;
12011        };
12012        self.advance(); // schema
12013        self.advance(); // dot
12014        self.advance(); // tbl
12015        Some(alloc::format!("{prefix}{normalised}"))
12016    }
12017
12018    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12019    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12020    /// implicit front of every search_path, so a bare reference to a
12021    /// known catalog table always means the catalog table. Only the
12022    /// names the engine actually synthesises are recognised — any
12023    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12024    fn try_peek_meta_bare(&mut self) -> Option<String> {
12025        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12026        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12027        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12028        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12029        // through the meta_view_result path instead, and already resolve
12030        // bare — they must NOT be listed here or the __spg_ rewrite would
12031        // mis-target them.)
12032        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12033        let name = match self.tokens.get(self.pos) {
12034            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12035            _ => return None,
12036        };
12037        // A following dot means this ident is a schema qualifier,
12038        // not a table name — let the qualified path handle it.
12039        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12040            return None;
12041        }
12042        if !PG_META_TABLES.contains(&name.as_str()) {
12043            return None;
12044        }
12045        self.advance();
12046        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12047        Some(alloc::format!("__spg_pg_{bare}"))
12048    }
12049
12050    /// Consume a bare ident if its lowercase matches `kw`, else err.
12051    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12052    /// Peeks only; the caller advances.
12053    fn peek_keyword_ident(&self, kw: &str) -> bool {
12054        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12055    }
12056
12057    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12058        match self.advance() {
12059            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12060            other => Err(ParseError {
12061                message: format!("expected {kw:?}, got {other:?}"),
12062                token_pos: self.consumed_pos(),
12063            }),
12064        }
12065    }
12066
12067    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12068    /// literal (`'foo'`) — same shape used by CREATE USER for the
12069    /// username slot.
12070    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12071        match self.advance() {
12072            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12073            other => Err(ParseError {
12074                message: format!("expected identifier or string, got {other:?}"),
12075                token_pos: self.consumed_pos(),
12076            }),
12077        }
12078    }
12079
12080    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12081        match self.advance() {
12082            Token::String(s) => Ok(s),
12083            other => Err(ParseError {
12084                message: format!("expected quoted string, got {other:?}"),
12085                token_pos: self.consumed_pos(),
12086            }),
12087        }
12088    }
12089
12090    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12091        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12092        // subqueries recurse through here without passing
12093        // parse_expr; share the same nesting budget.
12094        self.enter_nested()?;
12095        let r = self.parse_select_stmt_inner();
12096        self.nest_depth -= 1;
12097        r
12098    }
12099
12100    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12101        // Caller dispatches on Token::Select; the inner helper handles
12102        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12103        // get a fresh bare-select parse and may not have their own ORDER
12104        // BY / LIMIT.
12105        let mut head = self.parse_bare_select()?;
12106        self.parse_setop_chain_into(&mut head)?;
12107        self.parse_select_tail_into(&mut head)?;
12108        Ok(Statement::Select(head))
12109    }
12110
12111    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12112    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12113    /// token), and INTERSECT [ALL] (a bare ident — it was never
12114    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12115    /// tighter than UNION / EXCEPT — the executor folds the chain
12116    /// left-to-right, which is already correct for LEADING
12117    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12118    /// pair nests into that previous peer, so A UNION B INTERSECT C
12119    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12120    /// groups.
12121    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12122        // A parenthesized group arrives with its own (already
12123        // regrouped) unions on `head`; only the pairs THIS chain
12124        // appends participate in the precedence regroup below —
12125        // nesting an outer INTERSECT into a group-internal peer
12126        // would dissolve the explicit grouping.
12127        let boundary = head.unions.len();
12128        loop {
12129            let base = match self.peek() {
12130                Token::Union => UnionKind::Distinct,
12131                Token::Except => UnionKind::Except,
12132                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12133                _ => break,
12134            };
12135            self.advance();
12136            let kind = if matches!(self.peek(), Token::All) {
12137                self.advance();
12138                match base {
12139                    UnionKind::Distinct => UnionKind::All,
12140                    UnionKind::Except => UnionKind::ExceptAll,
12141                    _ => UnionKind::IntersectAll,
12142                }
12143            } else {
12144                base
12145            };
12146            let peer = self.parse_bare_select()?;
12147            head.unions.push((kind, peer));
12148        }
12149        let mut pairs = core::mem::take(&mut head.unions);
12150        let tail = pairs.split_off(boundary);
12151        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12152        for (kind, peer) in tail {
12153            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12154            // An intersect nests into the previous element of THIS
12155            // chain only; with no new previous element it stays at
12156            // the outer level (the left fold applies it to the
12157            // whole head, group included).
12158            match (
12159                is_intersect,
12160                regrouped.len() > boundary,
12161                regrouped.last_mut(),
12162            ) {
12163                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12164                _ => regrouped.push((kind, peer)),
12165            }
12166        }
12167        head.unions = regrouped;
12168        Ok(())
12169    }
12170
12171    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12172    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12173    /// the top-level bare VALUES statement reuses it verbatim.
12174    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12175    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12176    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12177    /// where the grouping-set universe is still in scope.
12178    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12179        if !matches!(self.peek(), Token::Order) {
12180            return Ok(Vec::new());
12181        }
12182        self.advance();
12183        if !self.peek_is_by() {
12184            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12185        }
12186        self.advance();
12187        let mut keys = Vec::new();
12188        loop {
12189            // v7.39 (round 691) — save/restore, the discipline this parser
12190            // already uses around `pending_sample_preds`, so a subquery inside
12191            // a key neither inherits nor leaks the channel.
12192            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12193            let saved_coll = self.order_key_collation.take();
12194            let parsed = self.parse_expr(0);
12195            self.in_order_by_key = saved_flag;
12196            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12197            let expr = parsed?;
12198            let desc = if matches!(self.peek(), Token::Desc) {
12199                self.advance();
12200                true
12201            } else if matches!(self.peek(), Token::Asc) {
12202                self.advance();
12203                false
12204            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12205                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12206                // one ordering per type, so the btree comparison operators map
12207                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12208                // would need a custom operator class — honest error.
12209                self.advance();
12210                match self.advance() {
12211                    Token::Lt | Token::LtEq => false,
12212                    Token::Gt | Token::GtEq => true,
12213                    other => {
12214                        return Err(self.err(alloc::format!(
12215                            "ORDER BY USING supports the btree comparison \
12216                             operators (< <= > >=); got {other:?}"
12217                        )));
12218                    }
12219                }
12220            } else {
12221                false
12222            };
12223            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12224            let nulls_first = self.parse_optional_nulls_placement()?;
12225            keys.push(OrderBy {
12226                expr,
12227                desc,
12228                nulls_first,
12229                collation,
12230            });
12231            if matches!(self.peek(), Token::Comma) {
12232                self.advance();
12233            } else {
12234                break;
12235            }
12236        }
12237        Ok(keys)
12238    }
12239
12240    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12241        // v7.39 (round 135) — a grouping-set query may have already parsed +
12242        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12243        // no ORDER BY token is present, keep that pre-set order_by rather than
12244        // clobbering it with an empty list.
12245        let parsed_keys = self.parse_order_by_keys()?;
12246        head.order_by = if parsed_keys.is_empty() {
12247            core::mem::take(&mut head.order_by)
12248        } else {
12249            parsed_keys
12250        };
12251        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12252        // order. PG's grammar takes a limit clause and an offset clause
12253        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12254        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12255        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12256        // spelling died on `expected end of input, got Limit`.
12257        //
12258        // Each may appear at most once, and LIMIT and FETCH FIRST are
12259        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12260        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12261        // A second one is left unconsumed here, which the caller reports
12262        // as trailing input rather than silently taking the last.
12263        let mut saw_limit = false;
12264        let mut saw_offset = false;
12265        loop {
12266            if !saw_limit && matches!(self.peek(), Token::Limit) {
12267                self.advance();
12268                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12269                // PG synonyms for "no limit". Treat both as None
12270                // (no head.limit set) so the engine's existing
12271                // unlimited-result path takes over. Reject was the
12272                // pre-5.1 behaviour and broke pg_dump-flavoured
12273                // tooling that occasionally emits LIMIT NULL.
12274                if self.consume_limit_unbounded_sentinel() {
12275                    head.limit = None;
12276                } else {
12277                    let first = self.parse_limit_expr("LIMIT")?;
12278                    // MySQL `LIMIT offset, count` — the first number is
12279                    // the offset when a comma follows.
12280                    if matches!(self.peek(), Token::Comma) {
12281                        self.advance();
12282                        let count = self.parse_limit_expr("LIMIT")?;
12283                        head.offset = Some(first);
12284                        saw_offset = true;
12285                        head.limit = Some(count);
12286                    } else {
12287                        head.limit = Some(first);
12288                    }
12289                }
12290                saw_limit = true;
12291                continue;
12292            }
12293            if !saw_offset && matches!(self.peek(), Token::Offset) {
12294                self.advance();
12295                // PG also accepts an optional `ROW` / `ROWS` trailer
12296                // after the offset value (`OFFSET 10 ROWS`). The
12297                // FETCH-FIRST branch below relies on the same.
12298                let off = self.parse_limit_expr("OFFSET")?;
12299                self.consume_optional_rows_keyword();
12300                head.offset = Some(off);
12301                saw_offset = true;
12302                continue;
12303            }
12304            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12305            // the SQL-standard alias for LIMIT. PG accepts both
12306            // spellings interchangeably; pg_dump emits FETCH FIRST in
12307            // newer versions. We map it onto `head.limit` so the
12308            // engine path is unified.
12309            if !saw_limit
12310                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12311                    if s.eq_ignore_ascii_case("fetch"))
12312            {
12313                self.advance(); // FETCH
12314                // `FIRST` or `NEXT` (both legal per SQL standard).
12315                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12316                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12317                {
12318                    self.advance();
12319                }
12320                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12321                // implicit 1 — but we always consume one if present).
12322                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12323                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12324                {
12325                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12326                    crate::ast::LimitExpr::Literal(1)
12327                } else {
12328                    self.parse_limit_expr("FETCH FIRST")?
12329                };
12330                // Eat `ROW` / `ROWS` if not already consumed above.
12331                self.consume_optional_rows_keyword();
12332                // Optional `ONLY` (the spec form) — or the SQL:2008
12333                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12334                // now honours WITH TIES by extending past the LIMIT
12335                // truncation point through every row that shares the
12336                // last-kept row's ORDER BY key.
12337                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12338                    if s.eq_ignore_ascii_case("only"))
12339                {
12340                    self.advance();
12341                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12342                    if s.eq_ignore_ascii_case("with"))
12343                {
12344                    self.advance(); // WITH
12345                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12346                        if s.eq_ignore_ascii_case("ties"))
12347                    {
12348                        self.advance();
12349                        head.limit_with_ties = true;
12350                    }
12351                }
12352                head.limit = Some(count);
12353                saw_limit = true;
12354                continue;
12355            }
12356            break;
12357        }
12358        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12359        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12360        //       [ OF table_name [, …] ]
12361        //       [ NOWAIT | SKIP LOCKED ]
12362        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12363        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12364        // SELECT already returns a consistent snapshot — so these
12365        // are accept-and-discard: the parser absorbs them so
12366        // mailrs / Rails / Django code paths that emit `SELECT
12367        // … FOR UPDATE` for advisory pessimistic locking load
12368        // without a parser error. The on-disk locking model is
12369        // unchanged; callers that rely on FOR UPDATE for read-
12370        // through-write ordering still get the right answer
12371        // because SPG serialises writes anyway.
12372        head.locking = self
12373            .consume_optional_for_lock_clauses()
12374            .map(alloc::boxed::Box::new);
12375        Ok(())
12376    }
12377
12378    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12379    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12380    /// LOCKED ]` trailers. Each clause is fully accepted and
12381    /// discarded — SPG's single-writer model already satisfies the
12382    /// callers' implicit ordering requirement. Stops at the first
12383    /// token that isn't `FOR`.
12384    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12385        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12386        // not discarded. PG keeps the strongest of several clauses; the
12387        // policy of the last one wins, which is what this loop records.
12388        let mut seen: Option<crate::ast::LockingClause> = None;
12389        while matches!(self.peek(), Token::For) {
12390            // v7.37.14 (A2.5-stub) — record that this query asked
12391            // for a row lock the parser is about to silently
12392            // discard. Operators surface the count via
12393            // `spg_sql::silent_for_update_count()` so they can
12394            // gauge how much of the workload depends on advisory
12395            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12396            // before v7.37.15's per-row tuple locking lands.
12397            crate::record_silent_for_update_clause();
12398            self.advance(); // FOR
12399            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12400            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12401            let mut no_key = false;
12402            let mut key = false;
12403            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12404                if s.eq_ignore_ascii_case("no"))
12405            {
12406                self.advance(); // NO
12407                no_key = true;
12408                // The next ident should be KEY but be generous;
12409                // anything followed by UPDATE/SHARE is accepted.
12410                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12411                    if s.eq_ignore_ascii_case("key"))
12412                {
12413                    self.advance(); // KEY
12414                }
12415            }
12416            // `KEY` prefix (PG `FOR KEY SHARE`).
12417            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418                if s.eq_ignore_ascii_case("key"))
12419            {
12420                self.advance(); // KEY
12421                key = true;
12422            }
12423            // Lock-strength keyword: UPDATE / SHARE. Required, but
12424            // we're lenient — an unexpected token here just bails
12425            // (we already consumed FOR; caller's downstream
12426            // dispatch will error if anything actually depends on
12427            // the trailing tokens).
12428            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12429                if s.eq_ignore_ascii_case("update"));
12430            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12431                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12432            {
12433                self.advance();
12434                use crate::ast::LockStrength as LS;
12435                let strength = match (is_update, no_key, key) {
12436                    (true, true, _) => LS::NoKeyUpdate,
12437                    (true, _, _) => LS::Update,
12438                    (false, _, true) => LS::KeyShare,
12439                    (false, _, _) => LS::Share,
12440                };
12441                seen = Some(crate::ast::LockingClause {
12442                    strength,
12443                    of_tables: alloc::vec::Vec::new(),
12444                    policy: crate::ast::LockWait::Wait,
12445                });
12446            } else {
12447                // FOR by itself (or `FOR KEY` with nothing after) —
12448                // give up on the lock-clause path. We've already
12449                // advanced past FOR; further attempts to parse
12450                // here would clobber state.
12451                return seen;
12452            }
12453            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12454            // joining and locking only a subset of tables.
12455            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12456                if s.eq_ignore_ascii_case("of"))
12457            {
12458                self.advance(); // OF
12459                #[allow(clippy::while_let_loop)]
12460                loop {
12461                    match self.peek() {
12462                        Token::Ident(_) | Token::QuotedIdent(_) => {
12463                            // v7.39 (round 294) — the name is CAPTURED now: PG
12464                            // validates it against the FROM clause, and an
12465                            // uncaptured list silently means "lock everything".
12466                            let mut nm = match self.advance() {
12467                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12468                                _ => alloc::string::String::new(),
12469                            };
12470                            // Optional schema-qualified `schema.table`.
12471                            if matches!(self.peek(), Token::Dot) {
12472                                self.advance();
12473                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12474                                {
12475                                    self.advance();
12476                                    nm = n;
12477                                }
12478                            }
12479                            if let Some(c) = seen.as_mut() {
12480                                c.of_tables.push(nm);
12481                            }
12482                        }
12483                        _ => break,
12484                    }
12485                    if matches!(self.peek(), Token::Comma) {
12486                        self.advance();
12487                    } else {
12488                        break;
12489                    }
12490                }
12491            }
12492            // Optional `NOWAIT` | `SKIP LOCKED`.
12493            match self.peek().clone() {
12494                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12495                    self.advance();
12496                    if let Some(c) = seen.as_mut() {
12497                        c.policy = crate::ast::LockWait::NoWait;
12498                    }
12499                }
12500                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12501                    self.advance(); // SKIP
12502                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12503                        if s.eq_ignore_ascii_case("locked"))
12504                    {
12505                        self.advance(); // LOCKED
12506                        if let Some(c) = seen.as_mut() {
12507                            c.policy = crate::ast::LockWait::SkipLocked;
12508                        }
12509                    }
12510                }
12511                _ => {}
12512            }
12513            // Loop: PG allows multiple FOR clauses chained.
12514        }
12515        seen
12516    }
12517
12518    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12519    /// Bind value gets resolved during prepared-statement Execute;
12520    /// the Pratt expression parser would over-accept here (e.g.
12521    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12522    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12523    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12524    /// when one was consumed; caller skips the regular
12525    /// limit-value parse and leaves `head.limit` at None.
12526    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12527        if matches!(self.peek(), Token::Null) {
12528            self.advance();
12529            return true;
12530        }
12531        if matches!(self.peek(), Token::All) {
12532            self.advance();
12533            return true;
12534        }
12535        false
12536    }
12537
12538    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12539    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12540    /// SQL-standard shape. No-op when missing.
12541    fn consume_optional_rows_keyword(&mut self) {
12542        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12543            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12544        {
12545            self.advance();
12546        }
12547    }
12548
12549    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12550    ///
12551    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12552    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12553    /// constant, which is why that spelling keeps the token path below.
12554    ///
12555    /// Constants are folded here rather than carried into the tree: the
12556    /// 15+ execution paths that read the row count go through
12557    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12558    /// means "no limit". A clause the engine could not resolve would
12559    /// therefore return the WHOLE table instead of failing. Folding at
12560    /// parse time keeps that impossible; a non-constant clause is still
12561    /// a clean error (recorded residual — closing it wants a resolution
12562    /// pre-pass on the simple-query path, where `substitute_placeholders`
12563    /// does not run).
12564    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12565        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12566        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12567        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12568        // ONLY` both work (its grammar takes a c_expr). Both measured
12569        // against PG 18.4 in round 305.
12570        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12571            return self.parse_limit_constant(label);
12572        }
12573        // One pass, no rewind: `advance()` takes each token by
12574        // `mem::replace`, so a consumed token reads back as Eof and this
12575        // parser cannot backtrack. Everything — bare literal included —
12576        // is therefore folded from the parsed expression rather than
12577        // re-read from the token stream.
12578        let start = self.pos;
12579        let e = self.parse_expr(0)?;
12580        if let crate::ast::Expr::Placeholder(n) = e {
12581            return Ok(crate::ast::LimitExpr::Placeholder(n));
12582        }
12583        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12584        match fold_limit_constant(&e) {
12585            Some(Ok(v)) if v < 0 => Err(ParseError {
12586                message: alloc::format!("{neg_label} must not be negative"),
12587                token_pos: start,
12588            }),
12589            Some(Ok(v)) => u32::try_from(v)
12590                .map(crate::ast::LimitExpr::Literal)
12591                .map_err(|_| ParseError {
12592                    message: alloc::format!("{label} value too large: {v}"),
12593                    token_pos: start,
12594                }),
12595            Some(Err(message)) => Err(ParseError {
12596                message: message.replace("{L}", neg_label),
12597                token_pos: start,
12598            }),
12599            // v7.39 (round 305, V23) — not foldable at parse time
12600            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12601            // expression; the engine evaluates it once before dispatch.
12602            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12603        }
12604    }
12605
12606    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12607        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12608        // coercion rules, not just an integer token: a NUMERIC rounds half
12609        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12610        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12611        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12612        // content, failing as an input-syntax error on the value. General
12613        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12614        // they need an Expr-carrying LimitExpr variant.
12615        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12616        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12617            message,
12618            token_pos: pos,
12619        };
12620        match self.advance() {
12621            Token::Integer(n) if n >= 0 => u32::try_from(n)
12622                .map(crate::ast::LimitExpr::Literal)
12623                .map_err(|_| ParseError {
12624                    message: alloc::format!("{label} value too large: {n}"),
12625                    token_pos: self.consumed_pos(),
12626                }),
12627            Token::Integer(_) => Err(err_at(
12628                alloc::format!("{neg_label} must not be negative"),
12629                self.pos.saturating_sub(1),
12630            )),
12631            Token::Numeric(t) => {
12632                let pos = self.pos.saturating_sub(1);
12633                let v: f64 = t.parse().map_err(|_| {
12634                    err_at(
12635                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12636                        pos,
12637                    )
12638                })?;
12639                if v < 0.0 {
12640                    return Err(err_at(
12641                        alloc::format!("{neg_label} must not be negative"),
12642                        pos,
12643                    ));
12644                }
12645                // Round half away from zero — PG's numeric→bigint cast.
12646                // (no_std: no f64::round; v is non-negative, so truncating
12647                // v + 0.5 is the same thing.)
12648                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12649                let rounded = (v + 0.5) as u64;
12650                u32::try_from(rounded)
12651                    .map(crate::ast::LimitExpr::Literal)
12652                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12653            }
12654            Token::Minus => {
12655                let pos = self.pos.saturating_sub(1);
12656                match self.peek() {
12657                    Token::Integer(_) | Token::Numeric(_) => {
12658                        self.advance();
12659                        Err(err_at(
12660                            alloc::format!("{neg_label} must not be negative"),
12661                            pos,
12662                        ))
12663                    }
12664                    other => Err(err_at(
12665                        alloc::format!(
12666                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12667                        ),
12668                        pos,
12669                    )),
12670                }
12671            }
12672            Token::String(t) => {
12673                let pos = self.pos.saturating_sub(1);
12674                match t.trim().parse::<i64>() {
12675                    Ok(n) if n < 0 => Err(err_at(
12676                        alloc::format!("{neg_label} must not be negative"),
12677                        pos,
12678                    )),
12679                    Ok(n) => u32::try_from(n)
12680                        .map(crate::ast::LimitExpr::Literal)
12681                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12682                    Err(_) => Err(err_at(
12683                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12684                        pos,
12685                    )),
12686                }
12687            }
12688            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12689            other => Err(ParseError {
12690                message: alloc::format!(
12691                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12692                ),
12693                token_pos: self.consumed_pos(),
12694            }),
12695        }
12696    }
12697
12698    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12699    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12700    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12701    /// `parse_select_stmt` is responsible for filling those in.
12702    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12703    /// call in the expression tree to the per-set integer bitmask
12704    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12705    /// is dropped in this grouping set). Runs during the ROLLUP /
12706    /// CUBE / GROUPING SETS expansion, where the set is known.
12707    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12708    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12709    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12710        if let Expr::FunctionCall { name, .. } = expr
12711            && name.eq_ignore_ascii_case("grouping")
12712        {
12713            if !out.iter().any(|e| e == expr) {
12714                out.push(expr.clone());
12715            }
12716            return;
12717        }
12718        match expr {
12719            Expr::Binary { lhs, rhs, .. } => {
12720                Self::collect_grouping_calls(lhs, out);
12721                Self::collect_grouping_calls(rhs, out);
12722            }
12723            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12724                Self::collect_grouping_calls(expr, out)
12725            }
12726            Expr::FunctionCall { args, .. } => {
12727                for a in args {
12728                    Self::collect_grouping_calls(a, out);
12729                }
12730            }
12731            Expr::Case {
12732                operand,
12733                branches,
12734                else_branch,
12735            } => {
12736                if let Some(o) = operand {
12737                    Self::collect_grouping_calls(o, out);
12738                }
12739                for (c, v) in branches {
12740                    Self::collect_grouping_calls(c, out);
12741                    Self::collect_grouping_calls(v, out);
12742                }
12743                if let Some(x) = else_branch {
12744                    Self::collect_grouping_calls(x, out);
12745                }
12746            }
12747            _ => {}
12748        }
12749    }
12750
12751    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12752    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12753    /// `__grp_ord_k` (injected per grouping-set branch).
12754    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12755        if let Expr::FunctionCall { name, .. } = expr
12756            && name.eq_ignore_ascii_case("grouping")
12757        {
12758            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12759                *expr = Expr::Column(crate::ast::ColumnName {
12760                    qualifier: None,
12761                    name: alloc::format!("__grp_ord_{k}"),
12762                });
12763            }
12764            return;
12765        }
12766        match expr {
12767            Expr::Binary { lhs, rhs, .. } => {
12768                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12769                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12770            }
12771            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12772                Self::rewrite_grouping_to_col(expr, grp_exprs)
12773            }
12774            Expr::FunctionCall { args, .. } => {
12775                for a in args {
12776                    Self::rewrite_grouping_to_col(a, grp_exprs);
12777                }
12778            }
12779            Expr::Case {
12780                operand,
12781                branches,
12782                else_branch,
12783            } => {
12784                if let Some(o) = operand {
12785                    Self::rewrite_grouping_to_col(o, grp_exprs);
12786                }
12787                for (c, v) in branches {
12788                    Self::rewrite_grouping_to_col(c, grp_exprs);
12789                    Self::rewrite_grouping_to_col(v, grp_exprs);
12790                }
12791                if let Some(x) = else_branch {
12792                    Self::rewrite_grouping_to_col(x, grp_exprs);
12793                }
12794            }
12795            _ => {}
12796        }
12797    }
12798
12799    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12800    /// as the list of key sets it contributes. A bare expression is one
12801    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12802    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12803    /// the concatenation of its items' sets, where an item is itself an
12804    /// element, a parenthesized key list, or the empty set `()`. A
12805    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12806    /// move together.
12807    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12808        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12809        // ROLLUP ( … ) / CUBE ( … )
12810        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12811            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12812        {
12813            let is_cube = is_kw(self.peek(), "cube");
12814            self.advance(); // ROLLUP / CUBE
12815            self.advance(); // (
12816            let mut units: Vec<Vec<Expr>> = Vec::new();
12817            loop {
12818                if matches!(self.peek(), Token::LParen) {
12819                    // Composite unit: (a, b) rolls up as one.
12820                    self.advance();
12821                    let mut unit = Vec::new();
12822                    if !matches!(self.peek(), Token::RParen) {
12823                        loop {
12824                            unit.push(self.parse_expr(0)?);
12825                            match self.peek() {
12826                                Token::Comma => {
12827                                    self.advance();
12828                                }
12829                                Token::RParen => break,
12830                                other => {
12831                                    return Err(self.err(format!(
12832                                        "expected ',' or ')' in grouping unit, got {other:?}"
12833                                    )));
12834                                }
12835                            }
12836                        }
12837                    }
12838                    self.advance(); // )
12839                    units.push(unit);
12840                } else {
12841                    units.push(alloc::vec![self.parse_expr(0)?]);
12842                }
12843                match self.peek() {
12844                    Token::Comma => {
12845                        self.advance();
12846                    }
12847                    Token::RParen => break,
12848                    other => {
12849                        return Err(self.err(format!(
12850                            "expected ',' or ')' in grouping list, got {other:?}"
12851                        )));
12852                    }
12853                }
12854            }
12855            self.advance(); // )
12856            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12857                units
12858                    .iter()
12859                    .zip(unit_sel.iter())
12860                    .filter(|(_, keep)| **keep)
12861                    .flat_map(|(u, _)| u.iter().cloned())
12862                    .collect()
12863            };
12864            let n = units.len();
12865            if is_cube {
12866                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12867                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12868                    .collect();
12869                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12870                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12871            }
12872            return Ok((0..=n)
12873                .rev()
12874                .map(|keep| {
12875                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12876                    flatten(&sel)
12877                })
12878                .collect());
12879        }
12880        // GROUPING SETS ( item [, item]* )
12881        if is_kw(self.peek(), "grouping")
12882            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12883        {
12884            self.advance(); // GROUPING
12885            self.advance(); // SETS
12886            if !matches!(self.peek(), Token::LParen) {
12887                return Err(self.err(format!(
12888                    "expected '(' after GROUPING SETS, got {:?}",
12889                    self.peek()
12890                )));
12891            }
12892            self.advance(); // outer (
12893            let mut sets: Vec<Vec<Expr>> = Vec::new();
12894            loop {
12895                if matches!(self.peek(), Token::LParen) {
12896                    // A parenthesized key list (or the empty set).
12897                    self.advance();
12898                    let mut set = Vec::new();
12899                    if !matches!(self.peek(), Token::RParen) {
12900                        loop {
12901                            set.push(self.parse_expr(0)?);
12902                            match self.peek() {
12903                                Token::Comma => {
12904                                    self.advance();
12905                                }
12906                                Token::RParen => break,
12907                                other => {
12908                                    return Err(self.err(format!(
12909                                        "expected ',' or ')' in grouping set, got {other:?}"
12910                                    )));
12911                                }
12912                            }
12913                        }
12914                    }
12915                    self.advance(); // )
12916                    sets.push(set);
12917                } else {
12918                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12919                    // bare expression.
12920                    sets.extend(self.parse_grouping_element()?);
12921                }
12922                match self.peek() {
12923                    Token::Comma => {
12924                        self.advance();
12925                    }
12926                    Token::RParen => break,
12927                    other => {
12928                        return Err(self.err(format!(
12929                            "expected ',' or ')' after a grouping set, got {other:?}"
12930                        )));
12931                    }
12932                }
12933            }
12934            self.advance(); // outer )
12935            return Ok(sets);
12936        }
12937        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12938    }
12939
12940    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12941        // v7.38 (read01) — a reference to a key that is dropped in this grouping
12942        // set evaluates to NULL, at any depth. Previously only a *top-level*
12943        // select item equal to a dropped key was nullified, so a key nested in
12944        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12945        // column and failed to resolve against the set's synthetic schema.
12946        if dropped.iter().any(|d| d == expr) {
12947            *expr = Expr::Literal(Literal::Null);
12948            return;
12949        }
12950        if let Expr::FunctionCall { name, args } = expr
12951            && name.eq_ignore_ascii_case("grouping")
12952        {
12953            let mut mask: i64 = 0;
12954            for a in args.iter() {
12955                mask <<= 1;
12956                if dropped.iter().any(|d| d == a) {
12957                    mask |= 1;
12958                }
12959            }
12960            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12961            // literal: a bare integer in a select item is indistinguishable
12962            // from a positional reference once `ORDER BY 1` substitutes the
12963            // item back in, and the round-232 position check then read the
12964            // mask value as an out-of-range position. The cast changes
12965            // nothing semantically (grouping() is integer).
12966            *expr = Expr::Cast {
12967                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12968                target: crate::ast::CastTarget::Int,
12969            };
12970            return;
12971        }
12972        // Generic recursion over the common expression shapes the
12973        // SELECT list uses; anything without child expressions is
12974        // left alone.
12975        match expr {
12976            Expr::FunctionCall { args, .. } => {
12977                for a in args {
12978                    Self::substitute_grouping_calls(a, dropped);
12979                }
12980            }
12981            Expr::Binary { lhs, rhs, .. } => {
12982                Self::substitute_grouping_calls(lhs, dropped);
12983                Self::substitute_grouping_calls(rhs, dropped);
12984            }
12985            Expr::Unary { expr: inner, .. } => {
12986                Self::substitute_grouping_calls(inner, dropped);
12987            }
12988            Expr::Cast { expr: inner, .. } => {
12989                Self::substitute_grouping_calls(inner, dropped);
12990            }
12991            Expr::Case {
12992                operand,
12993                branches,
12994                else_branch,
12995            } => {
12996                if let Some(op) = operand {
12997                    Self::substitute_grouping_calls(op, dropped);
12998                }
12999                for (w, t) in branches {
13000                    Self::substitute_grouping_calls(w, dropped);
13001                    Self::substitute_grouping_calls(t, dropped);
13002                }
13003                if let Some(e) = else_branch {
13004                    Self::substitute_grouping_calls(e, dropped);
13005                }
13006            }
13007            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13008            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13009            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13010            // …` is the canonical rollup-total label idiom).
13011            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13012            Expr::Like { expr, pattern, .. } => {
13013                Self::substitute_grouping_calls(expr, dropped);
13014                Self::substitute_grouping_calls(pattern, dropped);
13015            }
13016            Expr::InList { expr, list, .. } => {
13017                Self::substitute_grouping_calls(expr, dropped);
13018                for item in list {
13019                    Self::substitute_grouping_calls(item, dropped);
13020                }
13021            }
13022            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13023            Expr::Array(items) => {
13024                for item in items {
13025                    Self::substitute_grouping_calls(item, dropped);
13026                }
13027            }
13028            Expr::ArraySubscript { target, index } => {
13029                Self::substitute_grouping_calls(target, dropped);
13030                Self::substitute_grouping_calls(index, dropped);
13031            }
13032            Expr::ArraySlice { target, lo, hi } => {
13033                Self::substitute_grouping_calls(target, dropped);
13034                if let Some(lo) = lo {
13035                    Self::substitute_grouping_calls(lo, dropped);
13036                }
13037                if let Some(hi) = hi {
13038                    Self::substitute_grouping_calls(hi, dropped);
13039                }
13040            }
13041            Expr::AnyAll { expr, array, .. } => {
13042                Self::substitute_grouping_calls(expr, dropped);
13043                Self::substitute_grouping_calls(array, dropped);
13044            }
13045            _ => {}
13046        }
13047    }
13048
13049    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13050        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13051        // group: `( <select chain> )` usable anywhere a query block
13052        // is (head or peer of an outer chain). The group's own
13053        // unions ride the returned SelectStatement; the executor's
13054        // nested-peer recursion runs them.
13055        if matches!(self.peek(), Token::LParen)
13056            && matches!(
13057                self.tokens.get(self.pos + 1),
13058                Some(Token::Select | Token::LParen | Token::Values)
13059            )
13060        {
13061            self.advance(); // (
13062            self.enter_nested()?;
13063            // v7.37 D.20 — a group whose head is a VALUES list:
13064            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13065            // otherwise recurse into a nested SELECT/group head.
13066            let mut head = (if matches!(self.peek(), Token::Values) {
13067                self.advance(); // VALUES
13068                self.parse_values_rows_body()
13069            } else {
13070                self.parse_bare_select()
13071            })
13072            .and_then(|mut h| {
13073                self.parse_setop_chain_into(&mut h)?;
13074                Ok(h)
13075            });
13076            self.nest_depth -= 1;
13077            let mut head = match &mut head {
13078                Ok(h) => core::mem::take(h),
13079                Err(_) => return head,
13080            };
13081            // v7.37.17 (17.6 siblings) — group-internal tail:
13082            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13083            // group head, then wrap the group as a derived table
13084            // (SELECT * FROM (group)) so the outer chain / outer
13085            // tail can't clobber the group's own ordering or limit.
13086            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13087                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13088                    if s.eq_ignore_ascii_case("fetch"));
13089            if has_tail {
13090                self.parse_select_tail_into(&mut head)?;
13091                head = SelectStatement {
13092                    locking: None,
13093                    ctes: Vec::new(),
13094                    distinct: false,
13095                    distinct_on: Vec::new(),
13096                    items: alloc::vec![SelectItem::Wildcard],
13097                    from: Some(FromClause {
13098                        primary: TableRef {
13099                            name: "subquery".to_string(),
13100                            alias: None,
13101                            only: false,
13102                            as_of_segment: None,
13103                            unnest_expr: None,
13104                            unnest_column_aliases: Vec::new(),
13105                            with_ordinality: false,
13106                            generate_series_args: None,
13107                            lateral_subquery: Some(Box::new(head)),
13108                            jsonb_each_text_arg: None,
13109                            table_fn_call: None,
13110                            rows_from: None,
13111                            json_table: None,
13112                            scalar_fn_item: false,
13113                        },
13114                        joins: Vec::new(),
13115                    }),
13116                    where_: None,
13117                    group_by: None,
13118                    group_by_all: false,
13119                    having: None,
13120                    unions: Vec::new(),
13121                    order_by: Vec::new(),
13122                    limit: None,
13123                    offset: None,
13124                    limit_with_ties: false,
13125                    window_check_exprs: Vec::new(),
13126                };
13127            }
13128            if !matches!(self.peek(), Token::RParen) {
13129                return Err(self.err(format!(
13130                    "expected ')' after parenthesized query group, got {:?}",
13131                    self.peek()
13132                )));
13133            }
13134            self.advance();
13135            return Ok(head);
13136        }
13137        // `TABLE name` shorthand as a query block — valid anywhere
13138        // a SELECT head is (set-op peers included).
13139        if matches!(self.peek(), Token::Table)
13140            && matches!(
13141                self.tokens.get(self.pos + 1),
13142                Some(Token::Ident(_) | Token::QuotedIdent(_))
13143            )
13144        {
13145            return self.parse_table_shorthand();
13146        }
13147        if !matches!(self.peek(), Token::Select) {
13148            return Err(self.err(format!(
13149                "expected SELECT to start a query block, got {:?}",
13150                self.peek()
13151            )));
13152        }
13153        self.advance();
13154        let distinct = if matches!(self.peek(), Token::Distinct) {
13155            self.advance();
13156            true
13157        } else {
13158            false
13159        };
13160        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13161        // keep the first row (per ORDER BY) of each group the
13162        // expressions define. Django's .distinct('field') shape.
13163        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13164            self.advance(); // ON
13165            if !matches!(self.peek(), Token::LParen) {
13166                return Err(self.err(format!(
13167                    "expected '(' after DISTINCT ON, got {:?}",
13168                    self.peek()
13169                )));
13170            }
13171            self.advance();
13172            let mut exprs = Vec::new();
13173            loop {
13174                exprs.push(self.parse_expr(0)?);
13175                match self.peek() {
13176                    Token::Comma => {
13177                        self.advance();
13178                    }
13179                    Token::RParen => break,
13180                    other => {
13181                        return Err(self.err(format!(
13182                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13183                        )));
13184                    }
13185                }
13186            }
13187            self.advance(); // )
13188            exprs
13189        } else {
13190            Vec::new()
13191        };
13192        let mut items = self.parse_select_list()?;
13193        // Scope the TABLESAMPLE lowering channel to this SELECT:
13194        // stash whatever an enclosing select accumulated, collect
13195        // our own FROM's predicates, restore after the combine.
13196        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13197        let mut from = if matches!(self.peek(), Token::From) {
13198            self.advance();
13199            Some(self.parse_from_clause()?)
13200        } else {
13201            None
13202        };
13203        // v7.37 D.22 — a set-returning function in the projection with no FROM
13204        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13205        // rows. Move the first SRF projection item to a FROM-position derived
13206        // table and replace it in the projection with a reference to its output
13207        // column; sibling scalar columns repeat per SRF row. PG names the output
13208        // column after the function (or its AS alias). Reuses the FROM-SRF
13209        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13210        // works via the targetlist-SRF path.
13211        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13212        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13213        // is exactly what the function's own row shape already is. Anywhere else
13214        // (per outer row, or beside other items) it would need a real record-typed
13215        // projection, so it says so rather than answering something else.
13216        if let [
13217            SelectItem::Expr {
13218                expr: Expr::FunctionCall { name, args },
13219                ..
13220            },
13221        ] = items.as_slice()
13222            && name == "__record_expand"
13223        {
13224            let Some(Expr::FunctionCall {
13225                name: inner_name,
13226                args: inner_args,
13227            }) = args.first()
13228            else {
13229                return Err(self.err(
13230                    "(<expr>).* expands a function's record — it needs a function call".into(),
13231                ));
13232            };
13233            if from.is_some() {
13234                return Err(self.err(
13235                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13236                        .into(),
13237                ));
13238            }
13239            let fn_ref = TableRef {
13240                name: inner_name.clone(),
13241                alias: None,
13242                only: false,
13243                as_of_segment: None,
13244                unnest_expr: None,
13245                unnest_column_aliases: Vec::new(),
13246                with_ordinality: false,
13247                generate_series_args: None,
13248                lateral_subquery: None,
13249                jsonb_each_text_arg: None,
13250                table_fn_call: Some(Box::new((
13251                    inner_name.to_ascii_lowercase(),
13252                    inner_args.clone(),
13253                ))),
13254                rows_from: None,
13255                json_table: None,
13256                scalar_fn_item: false,
13257            };
13258            items = alloc::vec![SelectItem::Wildcard];
13259            from = Some(FromClause {
13260                primary: fn_ref,
13261                joins: Vec::new(),
13262            });
13263        }
13264        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13265        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13266        // record's fields takes the catalog. It becomes a LATERAL of the same
13267        // function plus one item per declared column — the machinery rounds 65
13268        // and 69 already built.
13269        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13270        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13271        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13272        // express, since the lifted one becomes a scan and the other would
13273        // expand per its rows (a cross product, not a zip). So when the
13274        // projection holds more than one top-level function call, the lift steps
13275        // aside and the engine's target-list expansion takes the whole list.
13276        let fn_call_items = items
13277            .iter()
13278            .filter(|it| {
13279                matches!(
13280                    it,
13281                    SelectItem::Expr {
13282                        expr: Expr::FunctionCall { .. },
13283                        ..
13284                    }
13285                )
13286            })
13287            .count();
13288        if from.is_none() && fn_call_items <= 1 {
13289            let mut found: Option<(usize, TableRef, String)> = None;
13290            for (i, item) in items.iter().enumerate() {
13291                if let SelectItem::Expr {
13292                    expr: Expr::FunctionCall { name, args },
13293                    alias,
13294                } = item
13295                {
13296                    let lname = name.to_ascii_lowercase();
13297                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13298                    let (unnest, gs) = match lname.as_str() {
13299                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13300                        "generate_series" if (2..=3).contains(&args.len()) => {
13301                            (None, Some(args.clone()))
13302                        }
13303                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13304                        // no-FROM projection yields the 1-based subscripts, i.e.
13305                        // generate_series(1, array_length(arr, dim)); an invalid
13306                        // dimension makes array_length NULL → 0 rows, as in PG.
13307                        "generate_subscripts" if args.len() == 2 => (
13308                            None,
13309                            Some(alloc::vec![
13310                                Expr::Literal(Literal::Integer(1)),
13311                                Expr::FunctionCall {
13312                                    name: "array_length".to_string(),
13313                                    args: args.clone(),
13314                                },
13315                            ]),
13316                        ),
13317                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13318                        // in a no-FROM projection unnest their *_to_array form.
13319                        "string_to_table" | "regexp_split_to_table" => {
13320                            let array_fn = if lname == "string_to_table" {
13321                                "string_to_array"
13322                            } else {
13323                                "regexp_split_to_array"
13324                            };
13325                            (
13326                                Some(Box::new(Expr::FunctionCall {
13327                                    name: array_fn.to_string(),
13328                                    args: args.clone(),
13329                                })),
13330                                None,
13331                            )
13332                        }
13333                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13334                        // a no-FROM projection expand per element. The scalar form
13335                        // returns the elements as a TEXT array, so unnest over the
13336                        // same call materialises one row each (same rewrite the
13337                        // FROM-clause form uses).
13338                        "jsonb_array_elements"
13339                        | "json_array_elements"
13340                        | "jsonb_array_elements_text"
13341                        | "json_array_elements_text"
13342                            if args.len() == 1 =>
13343                        {
13344                            (
13345                                Some(Box::new(Expr::FunctionCall {
13346                                    name: lname.clone(),
13347                                    args: args.clone(),
13348                                })),
13349                                None,
13350                            )
13351                        }
13352                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13353                        // in a no-FROM projection expands per match (scalar form
13354                        // returns the matches as a TEXT array → unnest).
13355                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13356                            Some(Box::new(Expr::FunctionCall {
13357                                name: lname.clone(),
13358                                args: args.clone(),
13359                            })),
13360                            None,
13361                        ),
13362                        _ => continue,
13363                    };
13364                    found = Some((
13365                        i,
13366                        TableRef {
13367                            name: colname.clone(),
13368                            alias: Some(colname.clone()),
13369                            only: false,
13370                            as_of_segment: None,
13371                            unnest_expr: unnest,
13372                            unnest_column_aliases: alloc::vec![colname.clone()],
13373                            with_ordinality: false,
13374                            generate_series_args: gs,
13375                            lateral_subquery: None,
13376                            jsonb_each_text_arg: None,
13377                            table_fn_call: None,
13378                            rows_from: None,
13379                            json_table: None,
13380                            scalar_fn_item: false,
13381                        },
13382                        colname,
13383                    ));
13384                    break;
13385                }
13386            }
13387            if let Some((idx, tref, colname)) = found {
13388                from = Some(FromClause {
13389                    primary: tref,
13390                    joins: Vec::new(),
13391                });
13392                items[idx] = SelectItem::Expr {
13393                    expr: Expr::Column(ColumnName {
13394                        qualifier: None,
13395                        name: colname.clone(),
13396                    }),
13397                    alias: Some(colname),
13398                };
13399            }
13400        }
13401        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13402        let where_ = if matches!(self.peek(), Token::Where) {
13403            self.advance();
13404            Some(self.parse_expr(0)?)
13405        } else {
13406            None
13407        };
13408        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13409            Some(match acc {
13410                Some(w) => Expr::Binary {
13411                    lhs: Box::new(pred),
13412                    op: crate::ast::BinOp::And,
13413                    rhs: Box::new(w),
13414                },
13415                None => pred,
13416            })
13417        });
13418        self.pending_sample_preds = enclosing_sample_preds;
13419        let mut group_by_all = false;
13420        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13421        // share one expansion: `grouping_sets` lists the key subsets
13422        // (first = primary, assigned to stmt.group_by; the rest
13423        // become UNION ALL peers), `grouping_universe` is the full
13424        // key list used to compute each peer's dropped keys.
13425        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13426        let mut grouping_universe: Vec<Expr> = Vec::new();
13427        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13428        // A BOOL, not the key list: this frame is the statement parser's, and
13429        // round 430 measured that a `Vec` local here is enough on its own to
13430        // tip the 512 KiB nesting guard. The keys are recoverable from
13431        // `grouping_universe`, which a rollup fills with exactly them.
13432        let mut mysql_rollup = false;
13433        let group_by = if matches!(self.peek(), Token::Group) {
13434            self.advance();
13435            if !self.peek_is_by() {
13436                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13437            }
13438            self.advance();
13439            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13440            // every non-aggregate SELECT-list item later.
13441            if matches!(self.peek(), Token::All) {
13442                self.advance();
13443                group_by_all = true;
13444                None
13445            } else {
13446                // v7.39 (round 242) — PG's general grouping-element grammar:
13447                // GROUP BY [DISTINCT] element [, element]*, where an element
13448                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13449                // SETS (…) — mixed freely. Each element yields a list of
13450                // key sets; the query's grouping sets are the CARTESIAN
13451                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13452                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13453                // content. ROLLUP/CUBE members may be composite
13454                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13455                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13456                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13457                // clause.
13458                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13459                    self.advance();
13460                    true
13461                } else {
13462                    false
13463                };
13464                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13465                loop {
13466                    element_sets.push(self.parse_grouping_element()?);
13467                    if matches!(self.peek(), Token::Comma) {
13468                        self.advance();
13469                    } else {
13470                        break;
13471                    }
13472                }
13473                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13474                for el in &element_sets {
13475                    let mut next: Vec<Vec<Expr>> = Vec::new();
13476                    for base in &total {
13477                        for set in el {
13478                            let mut merged = base.clone();
13479                            for k in set {
13480                                if !merged.iter().any(|m| m == k) {
13481                                    merged.push(k.clone());
13482                                }
13483                            }
13484                            next.push(merged);
13485                        }
13486                    }
13487                    total = next;
13488                }
13489                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13490                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13491                // The keys and the aggregates come out identical; the ROW
13492                // ORDER does not, and that is the part a report depends on.
13493                // MySQL interleaves each group's subtotal right after its
13494                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13495                // where the union-of-grouping-sets expansion emits every
13496                // leaf first and then every subtotal. MariaDB REFUSES an
13497                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13498                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13499                // agree on the order and disagree only on whether ORDER BY
13500                // is allowed (MySQL allows it; SPG allows it too, since
13501                // refusing would break the clients that can write it).
13502                if self.mysql_dialect
13503                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13504                    && matches!(
13505                        self.tokens.get(self.pos + 1),
13506                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13507                    )
13508                {
13509                    self.advance(); // WITH
13510                    self.advance(); // ROLLUP
13511                    let keys = total.into_iter().next().unwrap_or_default();
13512                    mysql_rollup = true;
13513                    // n+1 prefixes, largest first — the same expansion
13514                    // `ROLLUP (…)` produces.
13515                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13516                }
13517                if distinct_sets {
13518                    let mut seen: Vec<Vec<String>> = Vec::new();
13519                    total.retain(|set| {
13520                        let mut key: Vec<String> =
13521                            set.iter().map(|e| alloc::format!("{e}")).collect();
13522                        key.sort();
13523                        if seen.contains(&key) {
13524                            false
13525                        } else {
13526                            seen.push(key);
13527                            true
13528                        }
13529                    });
13530                }
13531                if total.len() > 1 {
13532                    let mut universe: Vec<Expr> = Vec::new();
13533                    for set in &total {
13534                        for k in set {
13535                            if !universe.iter().any(|u| u == k) {
13536                                universe.push(k.clone());
13537                            }
13538                        }
13539                    }
13540                    grouping_universe = universe;
13541                    let primary = total[0].clone();
13542                    grouping_sets = total;
13543                    Some(primary)
13544                } else {
13545                    // One set (a plain GROUP BY list, or a single-set
13546                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13547                    // single set — GROUPING SETS (()) — stays
13548                    // `Some(vec![])`: the grand-total group, which must
13549                    // run the aggregate path.
13550                    Some(total.into_iter().next().unwrap_or_default())
13551                }
13552            }
13553        } else {
13554            None
13555        };
13556        let having = if matches!(self.peek(), Token::Having) {
13557            self.advance();
13558            Some(self.parse_expr(0)?)
13559        } else {
13560            None
13561        };
13562        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13563        // OVER w parsed to a marker above; inline each definition
13564        // into the referencing WindowFunction nodes.
13565        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13566        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13567            self.advance();
13568            loop {
13569                let wname = self.expect_ident_like()?;
13570                if !matches!(self.peek(), Token::As) {
13571                    return Err(self.err(format!(
13572                        "expected AS after WINDOW {wname}, got {:?}",
13573                        self.peek()
13574                    )));
13575                }
13576                self.advance();
13577                // v7.39 (round 229) — PG rejects a redefinition outright.
13578                if window_defs
13579                    .iter()
13580                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13581                {
13582                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13583                }
13584                let def = self.parse_over_clause()?;
13585                // A definition may itself copy an earlier one
13586                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13587                // so resolve it against the defs already in scope. Same
13588                // copy rules as an `OVER (w1 …)` in the select list.
13589                let mut probe = Expr::WindowFunction {
13590                    name: String::new(),
13591                    args: Vec::new(),
13592                    partition_by: def.0,
13593                    order_by: def.1,
13594                    frame: def.2,
13595                    null_treatment: crate::ast::NullTreatment::Respect,
13596                    filter: None,
13597                };
13598                Self::substitute_named_windows(&mut probe, &window_defs)
13599                    .map_err(|m| self.err(m))?;
13600                let Expr::WindowFunction {
13601                    partition_by,
13602                    order_by,
13603                    frame,
13604                    ..
13605                } = probe
13606                else {
13607                    unreachable!("probe is a WindowFunction")
13608                };
13609                window_defs.push((wname, (partition_by, order_by, frame)));
13610                if matches!(self.peek(), Token::Comma) {
13611                    self.advance();
13612                    continue;
13613                }
13614                break;
13615            }
13616        }
13617        // v7.39 (round 705) — which definitions did anything reference?
13618        // The ones nothing did used to be dropped here, unexamined, so
13619        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13620        // definition whether referenced or not. Their key expressions ride
13621        // out on the statement for the engine to resolve.
13622        let mut window_refs: Vec<String> = Vec::new();
13623        if !window_defs.is_empty() {
13624            for it in &items {
13625                if let SelectItem::Expr { expr, .. } = it {
13626                    Self::collect_named_window_refs(expr, &mut window_refs);
13627                }
13628            }
13629        }
13630        let window_check_exprs: Vec<Expr> = window_defs
13631            .iter()
13632            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13633            .flat_map(|(_, (partition, order, _))| {
13634                partition
13635                    .iter()
13636                    .cloned()
13637                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13638            })
13639            .collect();
13640        if !window_defs.is_empty()
13641            || items
13642                .iter()
13643                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13644        {
13645            for it in &mut items {
13646                if let SelectItem::Expr { expr, .. } = it {
13647                    Self::substitute_named_windows(expr, &window_defs)
13648                        .map_err(|m| self.err(m))?;
13649                }
13650            }
13651        }
13652        // `GROUP BY 1` — positional keys substitute with the Nth
13653        // select item's expression (same contract ORDER BY has had
13654        // since v6.x). Out-of-range positions error.
13655        let group_by = match group_by {
13656            Some(mut keys) => {
13657                for k in &mut keys {
13658                    if let Expr::Literal(Literal::Integer(n)) = k {
13659                        let idx = *n;
13660                        if idx < 1 || idx as usize > items.len() {
13661                            return Err(self.err(alloc::format!(
13662                                "GROUP BY position {idx} is not in select list"
13663                            )));
13664                        }
13665                        match &items[(idx - 1) as usize] {
13666                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13667                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13668                                return Err(self.err(alloc::format!(
13669                                    "GROUP BY position {idx} references a wildcard item"
13670                                )));
13671                            }
13672                        }
13673                    }
13674                }
13675                Some(keys)
13676            }
13677            None => None,
13678        };
13679        let mut stmt = SelectStatement {
13680            locking: None,
13681            ctes: Vec::new(),
13682            distinct,
13683            distinct_on,
13684            items,
13685            from,
13686            where_,
13687            group_by,
13688            group_by_all,
13689            having,
13690            unions: Vec::new(),
13691            order_by: Vec::new(),
13692            limit: None,
13693            offset: None,
13694            limit_with_ties: false,
13695            window_check_exprs,
13696        };
13697        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13698        // first set is the primary (already on stmt.group_by); each
13699        // further set becomes a UNION ALL peer with its dropped
13700        // keys (universe minus the set) replaced by NULL literals
13701        // in the peer's items and group_by. PG-legal: non-grouped
13702        // select items must be group keys or aggregates, so a
13703        // dropped key's occurrences in the projection are exactly
13704        // the ones to nullify.
13705        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13706        // over a plain GROUP BY (every argument must be a group key; the
13707        // mask is then 0) and rejects anything else with 42803. SPG's
13708        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13709        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13710        // function `grouping`".
13711        if grouping_sets.len() <= 1 {
13712            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13713            let mut calls: Vec<Expr> = Vec::new();
13714            for item in &stmt.items {
13715                if let SelectItem::Expr { expr, .. } = item {
13716                    Self::collect_grouping_calls(expr, &mut calls);
13717                }
13718            }
13719            if let Some(h) = &stmt.having {
13720                Self::collect_grouping_calls(h, &mut calls);
13721            }
13722            for call in &calls {
13723                let Expr::FunctionCall { args, .. } = call else {
13724                    continue;
13725                };
13726                for a in args {
13727                    if !keys.iter().any(|k| k == a) {
13728                        return Err(self.err(
13729                            "arguments to GROUPING must be grouping expressions of the associated query level"
13730                                .to_string(),
13731                        ));
13732                    }
13733                }
13734            }
13735            if !calls.is_empty() {
13736                for item in &mut stmt.items {
13737                    if let SelectItem::Expr { expr, .. } = item {
13738                        Self::substitute_grouping_calls(expr, &[]);
13739                    }
13740                }
13741                if let Some(h) = &mut stmt.having {
13742                    Self::substitute_grouping_calls(h, &[]);
13743                }
13744            }
13745        }
13746        if grouping_sets.len() > 1 {
13747            // The primary set's own dropped keys nullify in the
13748            // HEAD's projection too (GROUPING SETS's first set may
13749            // omit keys other sets use).
13750            let primary = grouping_sets[0].clone();
13751            let head_dropped: Vec<Expr> = grouping_universe
13752                .iter()
13753                .filter(|u| !primary.iter().any(|k| k == *u))
13754                .cloned()
13755                .collect();
13756            for set in grouping_sets.iter().skip(1) {
13757                let mut peer = stmt.clone();
13758                peer.unions = Vec::new();
13759                let dropped: Vec<&Expr> = grouping_universe
13760                    .iter()
13761                    .filter(|u| !set.iter().any(|k| k == *u))
13762                    .collect();
13763                // Empty set = grand-total group: `Some(vec![])` forces
13764                // the aggregate path (one collapsed row) instead of a
13765                // per-row passthrough. See the primary-set note above.
13766                peer.group_by = Some(set.clone());
13767                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13768                for item in &mut peer.items {
13769                    if let SelectItem::Expr { expr, alias } = item {
13770                        if dropped.iter().any(|d| *d == expr) {
13771                            // v7.39 — keep the dropped key's name on the
13772                            // NULL literal so the UNION output column
13773                            // (and any top-level ORDER BY on it) still
13774                            // resolves.
13775                            if alias.is_none()
13776                                && let Expr::Column(c) = &expr
13777                            {
13778                                *alias = Some(c.name.clone());
13779                            }
13780                            *expr = Expr::Literal(Literal::Null);
13781                        } else {
13782                            Self::substitute_grouping_calls(expr, &dropped_owned);
13783                        }
13784                    }
13785                }
13786                if let Some(h) = &mut peer.having {
13787                    Self::substitute_grouping_calls(h, &dropped_owned);
13788                }
13789                stmt.unions.push((UnionKind::All, peer));
13790            }
13791            for item in &mut stmt.items {
13792                if let SelectItem::Expr { expr, alias } = item {
13793                    if head_dropped.iter().any(|d| d == expr) {
13794                        if alias.is_none()
13795                            && let Expr::Column(c) = &expr
13796                        {
13797                            *alias = Some(c.name.clone());
13798                        }
13799                        *expr = Expr::Literal(Literal::Null);
13800                    } else {
13801                        Self::substitute_grouping_calls(expr, &head_dropped);
13802                    }
13803                }
13804            }
13805            if let Some(h) = &mut stmt.having {
13806                Self::substitute_grouping_calls(h, &head_dropped);
13807            }
13808            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13809            // (while `grouping_universe` / the per-branch sets are in scope). For
13810            // each grouping() call in it, inject a per-branch hidden column
13811            // `__grp_ord_K` carrying that branch's mask into the head + every
13812            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13813            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13814            // from the final output. A standalone grouping-set query has ORDER BY
13815            // (not an explicit set-op) next, so consuming it here is safe.
13816            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13817            // rollup carries the hierarchical order: sort by the grouping
13818            // keys with the rolled-up NULLs last, which is exactly the
13819            // interleaving both oracles emit. A client's own ORDER BY wins,
13820            // which is what MySQL does (MariaDB refuses to let one be
13821            // written at all).
13822            // The synthesised keys have to travel the SAME path a written
13823            // ORDER BY does: the block below is what turns a `grouping()`
13824            // call into the per-branch `__grp_ord_K` column the engine can
13825            // actually sort on. Bypassing it left a bare `grouping(text)`
13826            // for the evaluator to reject.
13827            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13828                self.parse_order_by_keys()?
13829            } else if mysql_rollup {
13830                Self::mysql_rollup_order(&grouping_universe)
13831            } else {
13832                Vec::new()
13833            };
13834            if !synthesised_or_parsed.is_empty() {
13835                let mut order_keys = synthesised_or_parsed;
13836                let mut grp_exprs: Vec<Expr> = Vec::new();
13837                for ob in &order_keys {
13838                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13839                }
13840                for (k, gexpr) in grp_exprs.iter().enumerate() {
13841                    let colname = alloc::format!("__grp_ord_{k}");
13842                    // Head branch (primary set) uses `head_dropped`.
13843                    let mut he = gexpr.clone();
13844                    Self::substitute_grouping_calls(&mut he, &head_dropped);
13845                    stmt.items.push(SelectItem::Expr {
13846                        expr: he,
13847                        alias: Some(colname.clone()),
13848                    });
13849                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13850                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13851                        let set = &grouping_sets[i + 1];
13852                        let dropped: Vec<Expr> = grouping_universe
13853                            .iter()
13854                            .filter(|u| !set.iter().any(|k| k == *u))
13855                            .cloned()
13856                            .collect();
13857                        let mut pe = gexpr.clone();
13858                        Self::substitute_grouping_calls(&mut pe, &dropped);
13859                        peer.items.push(SelectItem::Expr {
13860                            expr: pe,
13861                            alias: Some(colname.clone()),
13862                        });
13863                    }
13864                }
13865                for ob in &mut order_keys {
13866                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13867                }
13868                stmt.order_by = order_keys;
13869            }
13870        }
13871        Ok(stmt)
13872    }
13873
13874    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13875    /// as ORDER BY keys.
13876    ///
13877    /// Per key: the rollup marker, then the key. Sorting on the key alone
13878    /// is not enough, and a table with a NULL in it says why — MariaDB puts
13879    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13880    /// the ROLLUP-introduced NULL last, and both print as NULL.
13881    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13882    /// real group including the data-NULL one, 1 only for the row the
13883    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13884    /// rolls up to NULL|2, a|1, b|3, NULL|6.
13885    ///
13886    /// `#[inline(never)]`: its locals must not join the statement parser's
13887    /// frame, which round 430 measured sitting against the nesting guard.
13888    #[inline(never)]
13889    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13890        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13891        for e in keys {
13892            out.push(OrderBy {
13893                expr: Expr::FunctionCall {
13894                    name: "grouping".into(),
13895                    args: alloc::vec![e.clone()],
13896                },
13897                desc: false,
13898                nulls_first: None,
13899                collation: None,
13900            });
13901            out.push(OrderBy {
13902                expr: e.clone(),
13903                desc: false,
13904                // MySQL orders NULL first on an ascending key.
13905                nulls_first: Some(true),
13906                collation: None,
13907            });
13908        }
13909        out
13910    }
13911
13912    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13913    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13914    #[inline(never)]
13915    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13916        use crate::ast::MaintainKind;
13917        self.skip_paren_option_list();
13918        let kind = match self.peek() {
13919            // `TABLE` and `INDEX` lex as keywords, not identifiers.
13920            Token::Table | Token::Index => {
13921                self.advance();
13922                MaintainKind::ReindexRelation
13923            }
13924            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13925                "index" | "table" => {
13926                    self.advance();
13927                    MaintainKind::ReindexRelation
13928                }
13929                "schema" => {
13930                    self.advance();
13931                    MaintainKind::ReindexSchema
13932                }
13933                "system" | "database" => {
13934                    self.advance();
13935                    MaintainKind::Whole
13936                }
13937                // PG requires the object type; anything else is the
13938                // caller's problem, not something to swallow.
13939                _ => MaintainKind::ReindexRelation,
13940            },
13941            _ => MaintainKind::Whole,
13942        };
13943        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13944        // allows the plain form, so the modifier is recorded rather than
13945        // skipped. It still has no effect on how the reindex runs.
13946        let mut concurrently = false;
13947        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13948            self.advance();
13949            concurrently = true;
13950        }
13951        let target = self.take_optional_maintain_name();
13952        self.consume_until_statement_boundary();
13953        Ok(Statement::Maintain {
13954            kind,
13955            concurrently,
13956            target,
13957        })
13958    }
13959
13960    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13961    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13962    #[inline(never)]
13963    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13964        use crate::ast::MaintainKind;
13965        self.skip_paren_option_list();
13966        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13967            self.advance();
13968        }
13969        let target = self.take_optional_maintain_name();
13970        self.consume_until_statement_boundary();
13971        Ok(Statement::Maintain {
13972            kind: if target.is_some() {
13973                MaintainKind::ClusterRelation
13974            } else {
13975                MaintainKind::Whole
13976            },
13977            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13978            // transaction block quite happily (measured).
13979            concurrently: false,
13980            target,
13981        })
13982    }
13983
13984    /// The next token as a relation / schema name, when there is one.
13985    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13986        match self.peek() {
13987            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13988                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13989                _ => None,
13990            },
13991            _ => None,
13992        }
13993    }
13994
13995    /// A parenthesised option list, absorbed.
13996    fn skip_paren_option_list(&mut self) {
13997        if !matches!(self.peek(), Token::LParen) {
13998            return;
13999        }
14000        let mut depth = 0usize;
14001        loop {
14002            match self.advance() {
14003                Token::LParen => depth += 1,
14004                Token::RParen => {
14005                    depth -= 1;
14006                    if depth == 0 {
14007                        return;
14008                    }
14009                }
14010                Token::Eof => return,
14011                _ => {}
14012            }
14013        }
14014    }
14015
14016    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14017    /// column list.
14018    ///
14019    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14020    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14021    /// / ALL. The three that describe physical storage have no meaning
14022    /// here, so they parse and change nothing rather than making a
14023    /// dump that mentions them fail to load.
14024    ///
14025    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14026    /// parse chain the nesting sentinel is tuned against.
14027    #[inline(never)]
14028    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14029        self.advance(); // LIKE
14030        let source = self.expect_ident_like()?;
14031        let mut options = crate::ast::LikeOptions::default();
14032        loop {
14033            let including = match self.peek() {
14034                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14035                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14036                _ => break,
14037            };
14038            self.advance();
14039            // `ALL` lexes as its own keyword, not an identifier.
14040            let opt = if matches!(self.peek(), Token::All) {
14041                self.advance();
14042                alloc::string::String::from("all")
14043            } else {
14044                self.expect_ident_like()?
14045            };
14046            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14047                o.defaults = on;
14048                o.constraints = on;
14049                o.identity = on;
14050                o.generated = on;
14051                o.indexes = on;
14052                o.comments = on;
14053            };
14054            match opt.to_ascii_lowercase().as_str() {
14055                "all" => set(&mut options, including),
14056                "defaults" => options.defaults = including,
14057                "constraints" => options.constraints = including,
14058                "identity" => options.identity = including,
14059                "generated" => options.generated = including,
14060                "indexes" => options.indexes = including,
14061                "comments" => options.comments = including,
14062                // No storage model to copy into.
14063                "storage" | "statistics" | "compression" => {}
14064                other => {
14065                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14066                }
14067            }
14068        }
14069        Ok(crate::ast::LikeSpec {
14070            source,
14071            at,
14072            options,
14073        })
14074    }
14075
14076    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14077        // Caller already consumed CREATE; we're sitting on TABLE.
14078        debug_assert!(matches!(self.peek(), Token::Table));
14079        self.advance();
14080        let if_not_exists = self.consume_if_not_exists();
14081        let name = self.expect_ident_like()?;
14082        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14083        // child shape has no column list; the child inherits its
14084        // columns from the parent at engine-DDL time. Detect it
14085        // before the `(` requirement below.
14086        if matches!(self.peek(), Token::Partition)
14087            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14088        {
14089            self.advance(); // PARTITION
14090            self.advance(); // of
14091            let partition_of = self.parse_partition_of_tail()?;
14092            return Ok(Statement::CreateTable(CreateTableStatement {
14093                temporary: false,
14094                name,
14095                columns: Vec::new(),
14096                like_specs: Vec::new(),
14097                inherits: Vec::new(),
14098                if_not_exists,
14099                foreign_keys: Vec::new(),
14100                table_constraints: Vec::new(),
14101                partition_by: None,
14102                partition_of: Some(partition_of),
14103            }));
14104        }
14105        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14106        // the materialized-view materialisation path (run the SELECT, infer the
14107        // column types, create + populate the table) but marks the node so the
14108        // executor creates a plain table without a mat-view registry entry.
14109        if matches!(self.peek(), Token::As) {
14110            self.advance();
14111            let body_stmt = self.parse_select_stmt()?;
14112            let Statement::Select(body) = body_stmt else {
14113                return Err(self.err(format!(
14114                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14115                )));
14116            };
14117            let with_data = self.parse_optional_with_data(true)?;
14118            return Ok(Statement::CreateMaterializedView(
14119                crate::ast::CreateMaterializedViewStatement {
14120                    temporary: false,
14121                    name,
14122                    if_not_exists,
14123                    columns: Vec::new(),
14124                    body,
14125                    with_data,
14126                    as_plain_table: true,
14127                },
14128            ));
14129        }
14130        if !matches!(self.peek(), Token::LParen) {
14131            return Err(self.err(format!(
14132                "expected '(' after table name, got {:?}",
14133                self.peek()
14134            )));
14135        }
14136        self.advance();
14137        let mut columns = Vec::new();
14138        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14139        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14140        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14141        loop {
14142            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14143            // column list. It is how a child that adds nothing of its own is
14144            // written, and this loop demanded at least one entry: `syntax
14145            // error at or near ")"`. The child takes the parent's columns,
14146            // which the INHERITS clause already arranges.
14147            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14148                self.advance();
14149                break;
14150            }
14151            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14152            // clauses from column definitions. Constraints start
14153            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14154            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14155            // a column.
14156            if self.peek_table_level_pk_start() {
14157                table_constraints.push(self.parse_table_level_primary_key()?);
14158            } else if matches!(self.peek(), Token::Like) {
14159                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14160                // <opt> ]*`. The source table's shape lives in the catalog,
14161                // so this records the clause and the engine expands it.
14162                like_specs.push(self.parse_create_table_like(columns.len())?);
14163            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14164                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14165                table_constraints.push(self.parse_table_level_exclude()?);
14166            } else if self.peek_table_level_unique_start() {
14167                table_constraints.push(self.parse_table_level_unique()?);
14168            } else if self.peek_table_level_check_start() {
14169                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14170                table_constraints.push(self.parse_table_level_check()?);
14171            } else if self.peek_mysql_inline_key_start() {
14172                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14173                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14174                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14175                // inside the column list. Skip name + paren list;
14176                // for UNIQUE KEY, register as a UC.
14177                if let Some(uc) = self.parse_mysql_inline_key()? {
14178                    table_constraints.push(uc);
14179                }
14180            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14181                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14182                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14183                // CHECK is named, and the named-CONSTRAINT arm used
14184                // to accept FOREIGN KEY only. The name is accepted
14185                // and discarded — same handling as every other SPG
14186                // constraint name.
14187                self.advance(); // CONSTRAINT
14188                // v7.39 (read01 round 48) — the name is kept now: the schema
14189                // stores it, so DROP / RENAME CONSTRAINT can find it.
14190                let con_name = self.expect_ident_like()?;
14191                let mut tc = match kind {
14192                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14193                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14194                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14195                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14196                };
14197                match &mut tc {
14198                    crate::ast::TableConstraint::Check { name, .. }
14199                    | crate::ast::TableConstraint::Unique { name, .. }
14200                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14201                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14202                        *name = Some(con_name);
14203                    }
14204                    _ => {}
14205                }
14206                table_constraints.push(tc);
14207            } else if self.peek_constraint_or_fk_start() {
14208                foreign_keys.push(self.parse_table_level_fk()?);
14209            } else {
14210                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14211                // v7.13.0 — fold inline UNIQUE / CHECK column
14212                // constraints into table-level entries so the
14213                // engine path stays uniform.
14214                if col.is_unique {
14215                    table_constraints.push(crate::ast::TableConstraint::Unique {
14216                        name: None,
14217                        columns: alloc::vec![col.name.clone()],
14218                        nulls_not_distinct: col.unique_nulls_not_distinct,
14219                        deferrable: col.constraint_deferrable,
14220                        initially_deferred: col.constraint_initially_deferred,
14221                    });
14222                }
14223                if let Some(check_expr) = col.check.clone() {
14224                    table_constraints.push(crate::ast::TableConstraint::Check {
14225                        name: None,
14226                        expr: check_expr,
14227                        not_valid: false,
14228                    });
14229                }
14230                columns.push(col);
14231                if let Some(fk) = col_level_fk {
14232                    foreign_keys.push(fk);
14233                }
14234            }
14235            match self.peek() {
14236                Token::Comma => {
14237                    self.advance();
14238                }
14239                Token::RParen => {
14240                    self.advance();
14241                    break;
14242                }
14243                other => {
14244                    return Err(
14245                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14246                    );
14247                }
14248            }
14249        }
14250        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14251        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14252        // nothing is written between the parentheses.
14253        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14254        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14255        // empty parentheses were a parse error in their own right — quite apart
14256        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14257        // SPG does not have (filed separately).
14258        let _ = &like_specs;
14259        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14260        // It sits between the column list and the MySQL table options,
14261        // and it was a syntax error until this round.
14262        let mut inherits: Vec<String> = Vec::new();
14263        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14264            if k.eq_ignore_ascii_case("inherits"))
14265        {
14266            self.advance();
14267            if !matches!(self.peek(), Token::LParen) {
14268                return Err(self.err(alloc::format!(
14269                    "expected ( after INHERITS, got {:?}",
14270                    self.peek()
14271                )));
14272            }
14273            self.advance();
14274            loop {
14275                inherits.push(self.expect_ident_like()?);
14276                if matches!(self.peek(), Token::Comma) {
14277                    self.advance();
14278                    continue;
14279                }
14280                break;
14281            }
14282            if !matches!(self.peek(), Token::RParen) {
14283                return Err(self.err(alloc::format!(
14284                    "expected ) closing INHERITS, got {:?}",
14285                    self.peek()
14286                )));
14287            }
14288            self.advance();
14289        }
14290        // v7.14.0 — consume MySQL/MariaDB table options after the
14291        // closing `)`. mysqldump emits things like
14292        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14293        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14294        // SPG accepts all forms as no-ops (each option is
14295        // `<ident> [=] <ident-or-string>` separated by whitespace).
14296        self.consume_mysql_table_options();
14297        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14298        // SPG has no per-table reloptions, so accept and ignore them so a
14299        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14300        self.consume_with_reloptions();
14301        // v7.37.6-B — declarative-partition-parent suffix
14302        // (`PARTITION BY RANGE (key_col)`) sits after the column
14303        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14304        // and locks the key column at one ident; the engine then
14305        // verifies the column type is TIMESTAMPTZ.
14306        let partition_by = if matches!(self.peek(), Token::Partition) {
14307            self.advance(); // PARTITION
14308            if !self.peek_is_by() {
14309                return Err(self.err(format!(
14310                    "expected BY after PARTITION, got {:?}",
14311                    self.peek()
14312                )));
14313            }
14314            self.advance();
14315            Some(self.parse_partition_by_tail()?)
14316        } else {
14317            None
14318        };
14319        Ok(Statement::CreateTable(CreateTableStatement {
14320            temporary: false,
14321            name,
14322            columns,
14323            like_specs,
14324            inherits,
14325            if_not_exists,
14326            foreign_keys,
14327            table_constraints,
14328            partition_by,
14329            partition_of: None,
14330        }))
14331    }
14332
14333    /// v7.37.6-B — case-insensitive ident match helper for the
14334    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14335    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14336    /// didn't burn a global keyword slot for each (see the
14337    /// `Token::Partition` doc-comment in `lexer.rs`).
14338    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14339        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14340    }
14341
14342    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14343    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14344    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14345        use crate::ast::{PartitionBySpec, PartitionKindAst};
14346        let kind = match self.peek() {
14347            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14348                self.advance();
14349                PartitionKindAst::Range
14350            }
14351            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14352                self.advance();
14353                PartitionKindAst::List
14354            }
14355            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14356                self.advance();
14357                PartitionKindAst::Hash
14358            }
14359            other => {
14360                return Err(self.err(format!(
14361                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14362                )));
14363            }
14364        };
14365        if !matches!(self.peek(), Token::LParen) {
14366            return Err(self.err(format!(
14367                "expected '(' after PARTITION BY <strategy>, got {:?}",
14368                self.peek()
14369            )));
14370        }
14371        self.advance();
14372        let mut key_columns = Vec::new();
14373        loop {
14374            key_columns.push(self.expect_ident_like()?);
14375            match self.peek() {
14376                Token::Comma => {
14377                    self.advance();
14378                }
14379                Token::RParen => {
14380                    self.advance();
14381                    break;
14382                }
14383                other => {
14384                    return Err(self.err(format!(
14385                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14386                    )));
14387                }
14388            }
14389        }
14390        if key_columns.is_empty() {
14391            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14392        }
14393        Ok(PartitionBySpec { kind, key_columns })
14394    }
14395
14396    /// v7.37.6-B — after `PARTITION OF`, expect
14397    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14398    /// or
14399    ///   <parent> DEFAULT
14400    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14401        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14402        let parent_name = self.expect_ident_like()?;
14403        // v7.37.6-B rejects an explicit column list — the child
14404        // inherits from the parent. mailrs round-7 taught us that
14405        // CREATE TABLE-side schema reconciliation hides drift, so
14406        // we surface this as a parse error rather than silently
14407        // ignoring user columns.
14408        if matches!(self.peek(), Token::LParen) {
14409            return Err(self.err(
14410                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14411                 at v7.37.6-B; the child inherits its columns from the parent"
14412                    .to_string(),
14413            ));
14414        }
14415        let bounds = match self.peek() {
14416            Token::Default => {
14417                self.advance();
14418                PartitionOfBoundsAst::Default
14419            }
14420            Token::For => {
14421                self.advance();
14422                if !matches!(self.peek(), Token::Values) {
14423                    return Err(
14424                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14425                    );
14426                }
14427                self.advance();
14428                // WITH is not a reserved Token in the lexer — it lexes
14429                // as Token::Ident("with"). Disambiguate manually.
14430                let want_with = matches!(
14431                    self.peek(),
14432                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14433                );
14434                if want_with {
14435                    self.advance();
14436                    if !matches!(self.peek(), Token::LParen) {
14437                        return Err(self.err(format!(
14438                            "expected '(' after FOR VALUES WITH, got {:?}",
14439                            self.peek()
14440                        )));
14441                    }
14442                    self.advance();
14443                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14444                    loop {
14445                        let key = self.expect_ident_like()?;
14446                        let n = match self.peek().clone() {
14447                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14448                                self.advance();
14449                                v as u32
14450                            }
14451                            other => {
14452                                return Err(self.err(format!(
14453                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14454                                )));
14455                            }
14456                        };
14457                        match key.to_ascii_uppercase().as_str() {
14458                            "MODULUS" => modulus = Some(n),
14459                            "REMAINDER" => remainder = Some(n),
14460                            other => {
14461                                return Err(self.err(format!(
14462                                    "FOR VALUES WITH: unknown key {other:?}; \
14463                                     expected MODULUS or REMAINDER"
14464                                )));
14465                            }
14466                        }
14467                        match self.peek() {
14468                            Token::Comma => {
14469                                self.advance();
14470                            }
14471                            Token::RParen => {
14472                                self.advance();
14473                                break;
14474                            }
14475                            other => {
14476                                return Err(self.err(format!(
14477                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14478                                )));
14479                            }
14480                        }
14481                    }
14482                    let modulus = modulus
14483                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14484                    let remainder = remainder.ok_or_else(|| {
14485                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14486                    })?;
14487                    if modulus == 0 {
14488                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14489                    }
14490                    if remainder >= modulus {
14491                        return Err(self.err(format!(
14492                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14493                             must be < MODULUS ({modulus})"
14494                        )));
14495                    }
14496                    PartitionOfBoundsAst::Hash { modulus, remainder }
14497                } else {
14498                    match self.peek() {
14499                        Token::From => {
14500                            self.advance();
14501                            let lower = Box::new(self.parse_partition_bound_expr()?);
14502                            if !matches!(self.peek(), Token::To) {
14503                                return Err(self.err(format!(
14504                                    "expected TO after FROM (...), got {:?}",
14505                                    self.peek()
14506                                )));
14507                            }
14508                            self.advance();
14509                            let upper = Box::new(self.parse_partition_bound_expr()?);
14510                            PartitionOfBoundsAst::Range { lower, upper }
14511                        }
14512                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14513                        Token::In => {
14514                            self.advance();
14515                            if !matches!(self.peek(), Token::LParen) {
14516                                return Err(self.err(format!(
14517                                    "expected '(' after FOR VALUES IN, got {:?}",
14518                                    self.peek()
14519                                )));
14520                            }
14521                            self.advance();
14522                            let mut values = Vec::new();
14523                            loop {
14524                                values.push(self.parse_expr(0)?);
14525                                match self.peek() {
14526                                    Token::Comma => {
14527                                        self.advance();
14528                                    }
14529                                    Token::RParen => {
14530                                        self.advance();
14531                                        break;
14532                                    }
14533                                    other => {
14534                                        return Err(self.err(format!(
14535                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14536                                    )));
14537                                    }
14538                                }
14539                            }
14540                            if values.is_empty() {
14541                                return Err(self.err(
14542                                    "FOR VALUES IN requires at least one literal".to_string(),
14543                                ));
14544                            }
14545                            PartitionOfBoundsAst::List { values }
14546                        }
14547                        other => {
14548                            return Err(self.err(format!(
14549                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14550                            )));
14551                        }
14552                    }
14553                }
14554            }
14555            other => {
14556                return Err(self.err(format!(
14557                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14558                )));
14559            }
14560        };
14561        Ok(PartitionOfSpec {
14562            parent_name,
14563            bounds,
14564        })
14565    }
14566
14567    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14568    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14569    /// markers (no-arg builtins) so the engine resolves them
14570    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14571    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14572        if !matches!(self.peek(), Token::LParen) {
14573            return Err(self.err(format!(
14574                "expected '(' before partition bound, got {:?}",
14575                self.peek()
14576            )));
14577        }
14578        self.advance();
14579        let expr = match self.peek() {
14580            Token::Ident(s) | Token::QuotedIdent(s)
14581                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14582            {
14583                let name = s.to_ascii_uppercase();
14584                self.advance();
14585                crate::ast::Expr::FunctionCall {
14586                    name,
14587                    args: Vec::new(),
14588                }
14589            }
14590            _ => self.parse_expr(0)?,
14591        };
14592        if !matches!(self.peek(), Token::RParen) {
14593            return Err(self.err(format!(
14594                "expected ')' after partition bound, got {:?}",
14595                self.peek()
14596            )));
14597        }
14598        self.advance();
14599        Ok(expr)
14600    }
14601
14602    /// v7.14.0 — true when the next tokens look like an inline
14603    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14604    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14605    /// — each followed by an optional name + `(...)`. Critical:
14606    /// a column NAMED `key` / `index` (PG accepts as ident) must
14607    /// NOT be mistaken for the KEY constraint shape. We disambig
14608    /// by requiring the keyword to be followed by either `(` or
14609    /// `<ident> (`.
14610    fn peek_mysql_inline_key_start(&self) -> bool {
14611        let cur = self.peek();
14612        // Shapes:
14613        //   KEY (cols)
14614        //   KEY name (cols)
14615        //   INDEX (cols)
14616        //   INDEX name (cols)
14617        //   UNIQUE KEY [name] (cols)
14618        //   UNIQUE INDEX [name] (cols)
14619        //   FULLTEXT [KEY|INDEX] [name] (cols)
14620        //   SPATIAL [KEY|INDEX] [name] (cols)
14621        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14622            // tokens at skip = the position AFTER the index-form
14623            // keywords (KEY/INDEX) have been consumed.
14624            match self.tokens.get(skip) {
14625                Some(Token::LParen) => true,
14626                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14627                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14628                }
14629                _ => false,
14630            }
14631        };
14632        // `INDEX` lexes as Token::Index (reserved), not as
14633        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14634        // start; the peek helper below handles either.
14635        let is_key_or_index_tok = |t: &Token| -> bool {
14636            matches!(t, Token::Index)
14637                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14638        };
14639        match cur {
14640            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14641            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14642                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14643            }
14644            Token::Ident(s)
14645                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14646            {
14647                let nxt = self.tokens.get(self.pos + 1);
14648                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14649                    self.pos + 2
14650                } else {
14651                    self.pos + 1
14652                };
14653                after_keyword_followed_by_paren_or_ident_paren(after_after)
14654            }
14655            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14656                let nxt = self.tokens.get(self.pos + 1);
14657                if !nxt.is_some_and(is_key_or_index_tok) {
14658                    return false;
14659                }
14660                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14661            }
14662            _ => false,
14663        }
14664    }
14665
14666    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14667    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14668    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14669    /// returns Some(TableConstraint::Index) so the engine builds
14670    /// a real BTree index on the leading column (mysqldump
14671    /// `KEY idx_posts_author (author_id)` shape).
14672    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14673    /// (the storage layer has no matching AM).
14674    fn parse_mysql_inline_key(
14675        &mut self,
14676    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14677        // Detect UNIQUE prefix.
14678        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14679        {
14680            self.advance();
14681            true
14682        } else {
14683            false
14684        };
14685        // Consume FULLTEXT / SPATIAL prefix and record which one
14686        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14687        // dedicated TableConstraint variant so the engine can
14688        // build a tsvector-GIN; SPATIAL still has no matching
14689        // AM, so it falls back to accept-as-no-op.
14690        let mut is_fulltext = false;
14691        let mut is_spatial = false;
14692        if let Token::Ident(s) = self.peek().clone() {
14693            if s.eq_ignore_ascii_case("fulltext") {
14694                self.advance();
14695                is_fulltext = true;
14696            } else if s.eq_ignore_ascii_case("spatial") {
14697                self.advance();
14698                is_spatial = true;
14699            }
14700        }
14701        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14702        // (reserved); accept either token shape.
14703        match self.peek() {
14704            Token::Index => {
14705                self.advance();
14706            }
14707            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14708                self.advance();
14709            }
14710            other => {
14711                return Err(self.err(alloc::format!(
14712                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14713                )));
14714            }
14715        }
14716        // Optional index name (an ident before the `(`).
14717        // v7.15.0 — capture the name when present so the engine
14718        // builds the secondary index under the user's chosen
14719        // name (matches mysqldump's `KEY idx_x (col)` shape).
14720        let mut idx_name: Option<String> = None;
14721        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14722            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14723        {
14724            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14725                idx_name = Some(s);
14726            }
14727        }
14728        // Optional `USING BTREE` / `USING HASH` (MySQL).
14729        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14730            self.advance();
14731            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14732                self.advance();
14733            }
14734        }
14735        // Required column list `(col [, col]*)`.
14736        if !matches!(self.peek(), Token::LParen) {
14737            return Err(self.err(alloc::format!(
14738                "expected '(' in inline KEY/INDEX, got {:?}",
14739                self.peek()
14740            )));
14741        }
14742        self.advance();
14743        let mut cols: Vec<String> = Vec::new();
14744        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14745            self.advance();
14746            cols.push(s);
14747            // Skip optional `(length)` per-column prefix.
14748            if matches!(self.peek(), Token::LParen) {
14749                let mut depth = 1usize;
14750                self.advance();
14751                while depth > 0 {
14752                    match self.peek() {
14753                        Token::LParen => depth += 1,
14754                        Token::RParen => depth -= 1,
14755                        Token::Eof => break,
14756                        _ => {}
14757                    }
14758                    self.advance();
14759                }
14760            }
14761            // Skip optional ASC / DESC.
14762            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14763                || matches!(self.peek(), Token::Asc | Token::Desc)
14764            {
14765                self.advance();
14766            }
14767            if matches!(self.peek(), Token::Comma) {
14768                self.advance();
14769                continue;
14770            }
14771            break;
14772        }
14773        if matches!(self.peek(), Token::RParen) {
14774            self.advance();
14775        }
14776        // Trailing options on the inline index — comment / etc.
14777        // Skip until comma or `)`.
14778        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14779            self.advance();
14780        }
14781        if cols.is_empty() {
14782            return Ok(None);
14783        }
14784        if is_unique {
14785            // Carry the captured idx_name on UNIQUE too so future
14786            // engine work can name the underlying BTree
14787            // accordingly; today the unique-constraint installer
14788            // synthesises the name itself, but Display round-trip
14789            // benefits from preserving it.
14790            Ok(Some(crate::ast::TableConstraint::Unique {
14791                name: idx_name,
14792                columns: cols,
14793                nulls_not_distinct: false,
14794                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14795                deferrable: false,
14796                initially_deferred: false,
14797            }))
14798        } else if is_fulltext {
14799            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14800            // routes through `TableConstraint::FulltextIndex`;
14801            // the engine builds a tsvector-GIN over each named
14802            // column so MATCH AGAINST gets a real inverted
14803            // index instead of a silently-dropped declaration.
14804            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14805                name: idx_name,
14806                columns: cols,
14807            }))
14808        } else if is_spatial {
14809            // SPG has no native SPATIAL AM. Accept-as-no-op
14810            // (declaration is parsed, but no index is built).
14811            Ok(None)
14812        } else {
14813            // v7.15.0 — plain KEY / INDEX builds a real BTree
14814            // secondary index.
14815            Ok(Some(crate::ast::TableConstraint::Index {
14816                name: idx_name,
14817                columns: cols,
14818            }))
14819        }
14820    }
14821
14822    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14823    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14824    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14825    /// (in any order, separated by whitespace).
14826    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14827    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14828    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14829    /// bare ident here, and only the parenthesised form is reloptions (so this
14830    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14831    fn consume_with_reloptions(&mut self) {
14832        let is_with = matches!(
14833            self.peek(),
14834            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14835        );
14836        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14837            return;
14838        }
14839        self.advance(); // WITH
14840        self.advance(); // (
14841        let mut depth = 1u32;
14842        while depth > 0 && !matches!(self.peek(), Token::Eof) {
14843            match self.peek() {
14844                Token::LParen => depth += 1,
14845                Token::RParen => depth -= 1,
14846                _ => {}
14847            }
14848            self.advance();
14849        }
14850    }
14851
14852    fn consume_mysql_table_options(&mut self) {
14853        loop {
14854            // Heuristic: a table option is an ident (or `DEFAULT`
14855            // reserved keyword) followed by `=` and an
14856            // ident / string / integer.
14857            let name_lc = match self.peek().clone() {
14858                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14859                Token::Default => alloc::string::String::from("default"),
14860                _ => break,
14861            };
14862            let known = matches!(
14863                name_lc.as_str(),
14864                "engine"
14865                    | "default"
14866                    | "charset"
14867                    | "collate"
14868                    | "auto_increment"
14869                    | "row_format"
14870                    | "comment"
14871                    | "pack_keys"
14872                    | "stats_persistent"
14873                    | "stats_auto_recalc"
14874                    | "stats_sample_pages"
14875                    | "key_block_size"
14876                    | "tablespace"
14877                    | "min_rows"
14878                    | "max_rows"
14879                    | "checksum"
14880                    | "delay_key_write"
14881                    | "insert_method"
14882                    | "data"
14883                    | "index"
14884                    | "encryption"
14885                    | "compression"
14886            );
14887            if !known {
14888                break;
14889            }
14890            self.advance(); // option name
14891            // `DEFAULT` optional prefix is followed by `CHARSET` /
14892            // `COLLATE`; consume the next ident too.
14893            if name_lc == "default" {
14894                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14895                    self.advance();
14896                }
14897            }
14898            if matches!(self.peek(), Token::Eq) {
14899                self.advance();
14900            }
14901            match self.peek() {
14902                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14903                    self.advance();
14904                }
14905                _ => {}
14906            }
14907        }
14908    }
14909
14910    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14911    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14912    /// sure (otherwise a column literally named `primary` would
14913    /// be mistaken).
14914    fn peek_table_level_pk_start(&self) -> bool {
14915        let cur = self.peek();
14916        let nxt = self.tokens.get(self.pos + 1);
14917        let nxt2 = self.tokens.get(self.pos + 2);
14918        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14919        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14920        let is_lparen = matches!(nxt2, Some(Token::LParen));
14921        is_primary && is_key && is_lparen
14922    }
14923
14924    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14925    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14926    /// (mailrs round-5 G10).
14927    fn peek_table_level_unique_start(&self) -> bool {
14928        let cur = self.peek();
14929        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14930        if !is_unique {
14931            return false;
14932        }
14933        let n1 = self.tokens.get(self.pos + 1);
14934        // Plain `UNIQUE (…)`.
14935        if matches!(n1, Some(Token::LParen)) {
14936            return true;
14937        }
14938        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14939        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14940        if !is_nulls {
14941            return false;
14942        }
14943        let n2 = self.tokens.get(self.pos + 2);
14944        let n3 = self.tokens.get(self.pos + 3);
14945        let n4 = self.tokens.get(self.pos + 4);
14946        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14947        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14948            return true;
14949        }
14950        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14951        if matches!(n2, Some(Token::Not))
14952            && matches!(n3, Some(Token::Distinct))
14953            && matches!(n4, Some(Token::LParen))
14954        {
14955            return true;
14956        }
14957        false
14958    }
14959
14960    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14961        self.advance(); // PRIMARY
14962        self.advance(); // KEY
14963        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14964        // v7.39 (round 711) — the trailer's values are CARRIED now; round
14965        // 621 consumed and dropped them (the storing half of F08).
14966        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14967        Ok(crate::ast::TableConstraint::PrimaryKey {
14968            name: None,
14969            columns,
14970            deferrable,
14971            initially_deferred,
14972        })
14973    }
14974
14975    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14976        self.advance(); // UNIQUE
14977        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14978        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14979        // is `NULLS DISTINCT` per the SQL standard.
14980        let mut nulls_not_distinct = false;
14981        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14982            let n1 = self.tokens.get(self.pos + 1);
14983            let n2 = self.tokens.get(self.pos + 2);
14984            let is_not = matches!(n1, Some(Token::Not));
14985            let is_distinct = matches!(n2, Some(Token::Distinct));
14986            if is_not && is_distinct {
14987                self.advance(); // NULLS
14988                self.advance(); // NOT
14989                self.advance(); // DISTINCT
14990                nulls_not_distinct = true;
14991            } else if matches!(n1, Some(Token::Distinct)) {
14992                self.advance(); // NULLS
14993                self.advance(); // DISTINCT
14994            }
14995        }
14996        let columns = self.parse_paren_ident_list("UNIQUE")?;
14997        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14998        Ok(crate::ast::TableConstraint::Unique {
14999            name: None,
15000            columns,
15001            nulls_not_distinct,
15002            deferrable,
15003            initially_deferred,
15004        })
15005    }
15006
15007    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15008    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15009    /// expression.
15010    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15011    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15012    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15013    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15014    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15015    /// commit: `NOT` starts no other suffix here, but reading both
15016    /// tokens before advancing keeps the caller's error message intact
15017    /// if someone writes `NOT NULL` by mistake.
15018    fn parse_not_valid_suffix(&mut self) -> bool {
15019        if !matches!(self.peek(), Token::Not) {
15020            return false;
15021        }
15022        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15023        {
15024            return false;
15025        }
15026        self.advance();
15027        self.advance();
15028        true
15029    }
15030
15031    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15032        self.advance(); // EXCLUDE
15033        // Optional `USING <method>`.
15034        let mut method = None;
15035        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15036            self.advance();
15037            method = Some(match self.advance() {
15038                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15039                other => {
15040                    return Err(self.err(alloc::format!(
15041                        "expected index method after USING, got {other:?}"
15042                    )));
15043                }
15044            });
15045        }
15046        if !matches!(self.peek(), Token::LParen) {
15047            return Err(self.err(alloc::format!(
15048                "expected '(' after EXCLUDE, got {:?}",
15049                self.peek()
15050            )));
15051        }
15052        self.advance();
15053        let mut elements: Vec<(String, String)> = Vec::new();
15054        loop {
15055            let col = match self.advance() {
15056                Token::Ident(s) | Token::QuotedIdent(s) => s,
15057                other => {
15058                    return Err(self.err(alloc::format!(
15059                        "expected column name in EXCLUDE, got {other:?}"
15060                    )));
15061                }
15062            };
15063            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15064                return Err(self.err(alloc::format!(
15065                    "expected WITH after EXCLUDE column, got {:?}",
15066                    self.peek()
15067                )));
15068            }
15069            self.advance();
15070            let op = match self.advance() {
15071                Token::InetOverlap => String::from("&&"),
15072                Token::Intersects => String::from("?#"),
15073                Token::IsBelow => String::from("<^"),
15074                Token::IsAbove => String::from(">^"),
15075                Token::PatternLt => String::from("~<~"),
15076                Token::PatternLtEq => String::from("~<=~"),
15077                Token::PatternGt => String::from("~>~"),
15078                Token::PatternGtEq => String::from("~>=~"),
15079                Token::TsMatchOld => String::from("@@@"),
15080                Token::Eq => String::from("="),
15081                Token::JsonContains => String::from("@>"),
15082                Token::JsonContainedBy => String::from("<@"),
15083                Token::OverLeft => String::from("&<"),
15084                Token::OverRight => String::from("&>"),
15085                other => {
15086                    return Err(self.err(alloc::format!(
15087                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15088                    )));
15089                }
15090            };
15091            elements.push((col, op));
15092            if matches!(self.peek(), Token::Comma) {
15093                self.advance();
15094                continue;
15095            }
15096            break;
15097        }
15098        if !matches!(self.peek(), Token::RParen) {
15099            return Err(self.err(alloc::format!(
15100                "expected ')' to close EXCLUDE, got {:?}",
15101                self.peek()
15102            )));
15103        }
15104        self.advance();
15105        Ok(crate::ast::TableConstraint::Exclude {
15106            name: None,
15107            method,
15108            elements,
15109        })
15110    }
15111
15112    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15113        self.advance(); // CHECK
15114        if !matches!(self.peek(), Token::LParen) {
15115            return Err(self.err(alloc::format!(
15116                "expected '(' after CHECK, got {:?}",
15117                self.peek()
15118            )));
15119        }
15120        self.advance();
15121        let expr = self.parse_expr(0)?;
15122        if !matches!(self.peek(), Token::RParen) {
15123            return Err(self.err(alloc::format!(
15124                "expected ')' to close CHECK predicate, got {:?}",
15125                self.peek()
15126            )));
15127        }
15128        self.advance();
15129        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15130        // are no existing rows for PG to skip, so it rejects the suffix.
15131        Ok(crate::ast::TableConstraint::Check {
15132            name: None,
15133            expr,
15134            not_valid: false,
15135        })
15136    }
15137
15138    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15139    fn peek_table_level_check_start(&self) -> bool {
15140        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15141    }
15142
15143    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15144    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15145    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15146    /// own CONSTRAINT prefix).
15147    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15148        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15149            return None;
15150        }
15151        // tokens[pos+1] is the constraint name (any ident-like);
15152        // tokens[pos+2] is the kind keyword.
15153        match self.tokens.get(self.pos + 2) {
15154            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15155                Some(NamedTableConstraintKind::Check)
15156            }
15157            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15158                Some(NamedTableConstraintKind::Unique)
15159            }
15160            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15161                Some(NamedTableConstraintKind::PrimaryKey)
15162            }
15163            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15164                Some(NamedTableConstraintKind::Exclude)
15165            }
15166            _ => None,
15167        }
15168    }
15169
15170    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15171        if !matches!(self.peek(), Token::LParen) {
15172            return Err(self.err(alloc::format!(
15173                "expected '(' after {ctx}, got {:?}",
15174                self.peek()
15175            )));
15176        }
15177        self.advance();
15178        let mut out = Vec::new();
15179        loop {
15180            out.push(self.expect_ident_like()?);
15181            match self.peek() {
15182                Token::Comma => {
15183                    self.advance();
15184                }
15185                Token::RParen => {
15186                    self.advance();
15187                    break;
15188                }
15189                other => {
15190                    return Err(self.err(alloc::format!(
15191                        "expected ',' or ')' in {ctx} list, got {other:?}"
15192                    )));
15193                }
15194            }
15195        }
15196        if out.is_empty() {
15197            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15198        }
15199        Ok(out)
15200    }
15201
15202    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15203    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15204    /// table-level FK; a column def never starts with either keyword
15205    /// (column names are not in this reserved set).
15206    fn peek_constraint_or_fk_start(&self) -> bool {
15207        let is_constraint_kw = matches!(
15208            self.peek(),
15209            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15210        );
15211        let is_foreign_kw = matches!(
15212            self.peek(),
15213            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15214        );
15215        is_constraint_kw || is_foreign_kw
15216    }
15217
15218    /// v7.6.0 — parse a table-level FK clause:
15219    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15220    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15221    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15222        let mut name: Option<String> = None;
15223        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15224            self.advance();
15225            name = Some(self.expect_ident_like()?);
15226        }
15227        // `FOREIGN`
15228        match self.advance() {
15229            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15230            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15231        }
15232        // `KEY`
15233        match self.advance() {
15234            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15235            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15236        }
15237        // `(col, col, ...)`
15238        if !matches!(self.peek(), Token::LParen) {
15239            return Err(self.err(format!(
15240                "expected '(' after FOREIGN KEY, got {:?}",
15241                self.peek()
15242            )));
15243        }
15244        self.advance();
15245        let mut columns = Vec::new();
15246        loop {
15247            columns.push(self.expect_ident_like()?);
15248            match self.peek() {
15249                Token::Comma => {
15250                    self.advance();
15251                }
15252                Token::RParen => {
15253                    self.advance();
15254                    break;
15255                }
15256                other => {
15257                    return Err(self.err(format!(
15258                        "expected ',' or ')' in FK column list, got {other:?}"
15259                    )));
15260                }
15261            }
15262        }
15263        if columns.is_empty() {
15264            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15265        }
15266        let (
15267            parent_table,
15268            parent_columns,
15269            on_delete,
15270            on_update,
15271            match_type,
15272            deferrable,
15273            initially_deferred,
15274        ) = self.parse_references_tail(columns.len())?;
15275        Ok(ForeignKeyConstraint {
15276            name,
15277            columns,
15278            parent_table,
15279            parent_columns,
15280            on_delete,
15281            on_update,
15282            match_type,
15283            deferrable,
15284            initially_deferred,
15285        })
15286    }
15287
15288    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15289    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15290    /// the local column count, used to default the parent column
15291    /// list when omitted (SQL spec: parent's PK is implied).
15292    fn parse_references_tail(
15293        &mut self,
15294        expected_arity: usize,
15295    ) -> Result<
15296        (
15297            String,
15298            Vec<String>,
15299            FkAction,
15300            FkAction,
15301            crate::ast::MatchType,
15302            // v7.39 (round 288) — deferrable, initially_deferred.
15303            bool,
15304            bool,
15305        ),
15306        ParseError,
15307    > {
15308        match self.advance() {
15309            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15310            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15311        }
15312        let parent_table = self.expect_ident_like()?;
15313        let mut parent_columns: Vec<String> = Vec::new();
15314        if matches!(self.peek(), Token::LParen) {
15315            self.advance();
15316            loop {
15317                parent_columns.push(self.expect_ident_like()?);
15318                match self.peek() {
15319                    Token::Comma => {
15320                        self.advance();
15321                    }
15322                    Token::RParen => {
15323                        self.advance();
15324                        break;
15325                    }
15326                    other => {
15327                        return Err(self.err(format!(
15328                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15329                        )));
15330                    }
15331                }
15332            }
15333        }
15334        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15335            return Err(self.err(format!(
15336                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15337                expected_arity,
15338                parent_columns.len()
15339            )));
15340        }
15341        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15342        // it between the referenced column list and the ON / DEFERRABLE
15343        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15344        // is skipped when any referencing column is NULL), so SIMPLE —
15345        // the default, and the only spelling pg_dump emits — is accepted
15346        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15347        // mixed-NULL rule, which is not wired yet; reject them honestly
15348        // rather than silently applying SIMPLE (PG itself errors on
15349        // MATCH PARTIAL as "not yet implemented").
15350        let mut match_type = crate::ast::MatchType::Simple;
15351        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15352            self.advance();
15353            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15354            // SIMPLE / PARTIAL arrive as bare identifiers.
15355            let kind = match self.advance() {
15356                Token::Full => "FULL".to_string(),
15357                Token::Ident(s) => s.to_uppercase(),
15358                other => {
15359                    return Err(self.err(format!(
15360                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15361                    )));
15362                }
15363            };
15364            match kind.as_str() {
15365                "SIMPLE" => {} // Default — match_type stays Simple.
15366                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15367                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15368                "FULL" => match_type = crate::ast::MatchType::Full,
15369                "PARTIAL" => {
15370                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15371                }
15372                _ => {
15373                    return Err(self.err(format!(
15374                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15375                    )));
15376                }
15377            }
15378        }
15379        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15380        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15381        // <action>` / `ON UPDATE <action>` in either order. PG /
15382        // pg_dump emits the timing clause AFTER the ON clauses
15383        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15384        // but the SQL spec allows either order. We loop over
15385        // every possible trailer and dispatch on the next token,
15386        // stopping when nothing matches. Phase 3.1 changes the
15387        // bare DEFERRABLE form from hard-error to accept-as-
15388        // immediate; SPG is single-writer with no deferred-
15389        // constraint window so the runtime semantics are always
15390        // immediate even when INITIALLY DEFERRED is requested.
15391        // PG's default referential action (no ON DELETE / ON UPDATE
15392        // clause) is NO ACTION, not RESTRICT — the two enforce
15393        // identically in SPG (single-writer, no deferred window; see the
15394        // shared match arm in constraints.rs) but information_schema.
15395        // referential_constraints must report NO ACTION to match PG.
15396        let mut on_delete = FkAction::NoAction;
15397        let mut on_update = FkAction::NoAction;
15398        let mut seen_on_delete = false;
15399        let mut seen_on_update = false;
15400        let mut deferrable = false;
15401        let mut initially_deferred = false;
15402        loop {
15403            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15404            let before = self.pos;
15405            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15406            if self.pos != before {
15407                deferrable = d;
15408                initially_deferred = idef;
15409                continue;
15410            }
15411            // ON DELETE / ON UPDATE.
15412            if !matches!(self.peek(), Token::On) {
15413                break;
15414            }
15415            self.advance();
15416            let which = self.advance();
15417            let action = self.parse_fk_action()?;
15418            match which {
15419                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15420                    if seen_on_delete {
15421                        return Err(self.err("ON DELETE specified twice".into()));
15422                    }
15423                    seen_on_delete = true;
15424                    on_delete = action;
15425                }
15426                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15427                    if seen_on_update {
15428                        return Err(self.err("ON UPDATE specified twice".into()));
15429                    }
15430                    seen_on_update = true;
15431                    on_update = action;
15432                }
15433                other => {
15434                    return Err(
15435                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15436                    );
15437                }
15438            }
15439        }
15440        Ok((
15441            parent_table,
15442            parent_columns,
15443            on_delete,
15444            on_update,
15445            match_type,
15446            deferrable,
15447            initially_deferred,
15448        ))
15449    }
15450
15451    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15452    /// NO ACTION`.
15453    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15454        match self.advance() {
15455            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15456            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15457            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15458                Token::Null => Ok(FkAction::SetNull),
15459                Token::Default => Ok(FkAction::SetDefault),
15460                other => Err(self.err(format!(
15461                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15462                ))),
15463            },
15464            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15465                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15466                other => Err(self.err(format!(
15467                    "expected ACTION after NO in FK action, got {other:?}"
15468                ))),
15469            },
15470            other => Err(self.err(format!(
15471                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15472            ))),
15473        }
15474    }
15475
15476    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15477    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15478    fn consume_if_not_exists(&mut self) -> bool {
15479        // `IF` arrives as a bare Ident (we don't reserve it because it
15480        // also appears mid-expression in PG, though we don't support
15481        // those forms yet).
15482        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15483        if !looks_like_if {
15484            return false;
15485        }
15486        // Peek one ahead before committing: only consume IF when it's
15487        // actually `IF NOT EXISTS`.
15488        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15489            return false;
15490        }
15491        if !matches!(
15492            self.tokens.get(self.pos + 2),
15493            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15494        ) {
15495            return false;
15496        }
15497        self.advance(); // IF
15498        self.advance(); // NOT
15499        self.advance(); // EXISTS
15500        true
15501    }
15502
15503    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15504    /// Consumes IF EXISTS as a pair; returns false otherwise
15505    /// without consuming any tokens.
15506    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15507    /// ENABLE/DISABLE/FORCE/NO FORCE.
15508    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15509        for kw in ["row", "level", "security"] {
15510            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15511            {
15512                return Err(self.err(alloc::format!(
15513                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15514                    kw.to_ascii_uppercase(),
15515                    self.peek()
15516                )));
15517            }
15518            self.advance();
15519        }
15520        Ok(())
15521    }
15522
15523    fn consume_if_exists(&mut self) -> bool {
15524        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15525        if !looks_like_if {
15526            return false;
15527        }
15528        if !matches!(
15529            self.tokens.get(self.pos + 1),
15530            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15531        ) {
15532            return false;
15533        }
15534        self.advance(); // IF
15535        self.advance(); // EXISTS
15536        true
15537    }
15538
15539    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15540    /// qualifiers after an index column ref. ASC / DESC are
15541    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15542    /// We accept and discard them since single-column BTree
15543    /// stores rows in natural key order today.
15544    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15545    /// ORDER BY key. Returns None when absent.
15546    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15547        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15548            return Ok(None);
15549        }
15550        self.advance();
15551        match self.advance() {
15552            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15553            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15554            other => Err(self.err(alloc::format!(
15555                "expected FIRST or LAST after NULLS, got {other:?}"
15556            ))),
15557        }
15558    }
15559
15560    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15561    /// rather than discarded.
15562    ///
15563    /// SPG's index does not scan in a direction — column ordering is
15564    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15565    /// reproduction of the DDL, and dropping the clause meant
15566    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15567    /// dump lost it, and a schema diff saw drift on every run.
15568    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15569        let mut order = crate::ast::IndexColumnOrder::default();
15570        loop {
15571            match self.peek() {
15572                Token::Asc => {
15573                    self.advance();
15574                }
15575                Token::Desc => {
15576                    order.descending = true;
15577                    self.advance();
15578                }
15579                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15580                    let look = self.tokens.get(self.pos + 1);
15581                    if matches!(
15582                        look,
15583                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15584                            || k.eq_ignore_ascii_case("last")
15585                    ) {
15586                        self.advance();
15587                        order.nulls_first = Some(matches!(
15588                            self.advance(),
15589                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15590                        ));
15591                    } else {
15592                        break;
15593                    }
15594                }
15595                _ => break,
15596            }
15597        }
15598        order
15599    }
15600
15601    fn parse_create_index_stmt_after_create(
15602        &mut self,
15603        is_unique: bool,
15604    ) -> Result<Statement, ParseError> {
15605        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15606        debug_assert!(matches!(self.peek(), Token::Index));
15607        self.advance();
15608        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15609        // SPG's CREATE INDEX is synchronous end-to-end today (real
15610        // CONCURRENTLY variant with restartable scans queues with
15611        // v7.39 indexes epic), so the modifier has no runtime effect
15612        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15613        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15614        // VIEW CONCURRENTLY.
15615        let mut concurrently = false;
15616        if matches!(
15617            self.peek(),
15618            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15619        ) {
15620            self.advance();
15621            concurrently = true;
15622        }
15623        let if_not_exists = self.consume_if_not_exists();
15624        // v7.39 (read01 round 93) — the index name is optional (PG since
15625        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15626        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15627        // was given; leave it empty and the engine derives a PG-style
15628        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15629        let name = if matches!(self.peek(), Token::On) {
15630            String::new()
15631        } else {
15632            self.expect_ident_like()?
15633        };
15634        if !matches!(self.peek(), Token::On) {
15635            return Err(self.err(format!(
15636                "expected ON after CREATE INDEX <name>, got {:?}",
15637                self.peek()
15638            )));
15639        }
15640        self.advance();
15641        let table = self.expect_ident_like()?;
15642        // Optional `USING <method>` — only recognised method in v2.0 is
15643        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15644        // ident `using` (we don't promote it to a reserved keyword
15645        // because it isn't reserved anywhere else in our SQL surface).
15646        let mut method_name: Option<String> = None;
15647        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15648            self.advance();
15649            let m = self.expect_ident_like()?;
15650            method_name = Some(m.to_ascii_lowercase());
15651            match m.to_ascii_lowercase().as_str() {
15652                "hnsw" => IndexMethod::Hnsw,
15653                "btree" => IndexMethod::BTree,
15654                "brin" => IndexMethod::Brin,
15655                // v7.12.3 — real GIN inverted index over `tsvector`.
15656                // v7.9.26b's `USING gin` → BTree silent fallback is
15657                // gone; the engine validates that the indexed column
15658                // is `tsvector` at CREATE INDEX time.
15659                "gin" => IndexMethod::Gin,
15660                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15661                // `USING spgist` / `USING hash` for their built-in
15662                // AMs that SPG doesn't have a matching
15663                // implementation for; degrade to BTree on the
15664                // leading column so the schema loads + the index
15665                // catalogue stays consistent. Operator pays the
15666                // planner cost only for the queries that would have
15667                // used the specialised AM.
15668                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15669                // v7.11.3 — pgvector ships both `ivfflat` and
15670                // `hnsw`. Customers shouldn't have to choose
15671                // their on-disk index method based on what SPG
15672                // implements; accept `ivfflat` as a synonym for
15673                // `hnsw` so PG schemas using either method drop
15674                // in. The vector distance op (`<->` / `<#>` /
15675                // `<=>`) at query time still picks the metric.
15676                "ivfflat" => IndexMethod::Hnsw,
15677                other => {
15678                    return Err(self.err(alloc::format!(
15679                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15680                    )));
15681                }
15682            }
15683        } else {
15684            IndexMethod::BTree
15685        };
15686        if !matches!(self.peek(), Token::LParen) {
15687            return Err(self.err(format!(
15688                "expected '(' before indexed column, got {:?}",
15689                self.peek()
15690            )));
15691        }
15692        self.advance();
15693        // v6.8.2 — accept either a bare column ident (legacy) or
15694        // an expression `fn(col, …)` for expression indexes.
15695        // Distinguish by peeking the token *after* the current
15696        // ident: `ident )` is the legacy column-only path;
15697        // anything else triggers the Pratt expression parser.
15698        // (`advance()` uses `mem::replace` to nil out the current
15699        // slot, so we can't save+rewind cleanly — peek-ahead via
15700        // direct index avoids the mutation.)
15701        let mut opclass: Option<String> = None;
15702        let mut key_collation: Option<String> = None;
15703        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15704            // Single column with `)` immediately after — fast path.
15705            // v7.9.29 — also: bare column followed by `,` (the
15706            // multi-column form `(a, b, c)`). Without this branch
15707            // the leading ident gets pulled into `parse_expr`
15708            // which then sets `expression = Some(Column(a))` and
15709            // breaks Display round-trip on the multi-column shape.
15710            Token::Ident(s) | Token::QuotedIdent(s)
15711                if matches!(
15712                    self.tokens.get(self.pos + 1),
15713                    Some(Token::RParen | Token::Comma)
15714                ) =>
15715            {
15716                self.advance();
15717                (s, None)
15718            }
15719            // v7.9.22 — single column followed by a pgvector
15720            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15721            // v7.15.0 — capture the opclass instead of discarding
15722            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15723            // → real trigram-shingle GIN over a TEXT column).
15724            // Vector/HNSW opclasses still take their distance
15725            // metric from the query operator (`<->` / `<#>` /
15726            // `<=>`), so for those callers the opclass stays
15727            // informational.
15728            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15729            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15730            // the schema and dispatch on the bare opclass, the same
15731            // treatment table/type names get.
15732            Token::Ident(s) | Token::QuotedIdent(s)
15733                if matches!(
15734                    self.tokens.get(self.pos + 1),
15735                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15736                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15737                    && matches!(
15738                        self.tokens.get(self.pos + 3),
15739                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15740                            if is_vector_opclass_name(op)
15741                    ) =>
15742            {
15743                self.advance(); // column name
15744                self.advance(); // schema qualifier
15745                self.advance(); // dot
15746                let op_tok = self.advance();
15747                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15748                    opclass = Some(op.to_ascii_lowercase());
15749                }
15750                (s, None)
15751            }
15752            // r1038 — an operator class is recognised by its POSITION, not
15753            // by a list of names. It used to be `is_vector_opclass_name`,
15754            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15755            // sentori's migration wrote — was a syntax error while
15756            // `USING gin (doc)` parsed. Anything sitting between a column
15757            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15758            // two bare identifiers in a row are not valid there otherwise.
15759            Token::Ident(s) | Token::QuotedIdent(s)
15760                if matches!(
15761                    self.tokens.get(self.pos + 1),
15762                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15763                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
15764                            self.tokens.get(self.pos + 2)
15765                        )
15766                ) =>
15767            {
15768                self.advance(); // column name
15769                // Capture the opclass token, lower-cased for
15770                // case-insensitive engine dispatch.
15771                let op_tok = self.advance();
15772                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15773                    opclass = Some(op.to_ascii_lowercase());
15774                }
15775                (s, None)
15776            }
15777            Token::Ident(_) | Token::QuotedIdent(_) => {
15778                // v7.39 (round 538) — an explicit COLLATE on the key,
15779                // read by LOOKAHEAD because `parse_expr` absorbs the
15780                // clause as a no-op (SPG orders text by bytes, which is
15781                // the C collation, so it changes nothing to honour). PG
15782                // still PRINTS it: an explicitly written `"C"` and the
15783                // collation a column inherits are different collation
15784                // OBJECTS even where they sort identically, which is why
15785                // `(a COLLATE "C")` shows on a C-collation database too.
15786                if matches!(
15787                    self.tokens.get(self.pos + 1),
15788                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15789                ) {
15790                    key_collation = match self.tokens.get(self.pos + 2) {
15791                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15792                            Some(n.clone())
15793                        }
15794                        _ => None,
15795                    };
15796                }
15797                let key_expr = self.parse_expr(0)?;
15798                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15799                    self.err("expression index key must reference at least one column".into())
15800                })?;
15801                (primary, Some(key_expr))
15802            }
15803            // v7.37.43-T4 — parenthesised expression index key
15804            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15805            // PG's CREATE INDEX requires the expression to be in
15806            // its own parens to disambiguate function calls from
15807            // column lists, so this `LParen` is the inner open-paren
15808            // of an expression key. parse_expr handles the recursive
15809            // descent and consumes the matching `RParen`.
15810            Token::LParen => {
15811                let key_expr = self.parse_expr(0)?;
15812                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15813                    self.err("expression index key must reference at least one column".into())
15814                })?;
15815                (primary, Some(key_expr))
15816            }
15817            other => {
15818                return Err(self.err(format!(
15819                    "expected column ident or expression, got {other:?}"
15820                )));
15821            }
15822        };
15823        // v7.9.14 — accept extra comma-separated columns inside
15824        // the index key parens (`CREATE INDEX … (a, b, c)`).
15825        // mailrs F2. Each extra column may carry an optional
15826        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15827        // — parsed and discarded; SPG doesn't honour direction
15828        // on a BTree index today (column ordering is intrinsic
15829        // to the storage). v7.10 will widen to genuine composite
15830        // index keys.
15831        let mut extra_columns: Vec<String> = Vec::new();
15832        // The leading column may also have ASC/DESC after it — and that
15833        // one is the column SPG indexes, so its clause is kept.
15834        let key_order = self.consume_optional_index_column_qualifiers();
15835        while matches!(self.peek(), Token::Comma) {
15836            self.advance();
15837            let extra = self.expect_ident_like()?;
15838            let _ = self.consume_optional_index_column_qualifiers();
15839            extra_columns.push(extra);
15840        }
15841        if !matches!(self.peek(), Token::RParen) {
15842            return Err(self.err(format!(
15843                "expected ')' after indexed column / expression, got {:?}",
15844                self.peek()
15845            )));
15846        }
15847        self.advance();
15848        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15849        // index-only-scan annotation. Bare ident (not a reserved
15850        // keyword) so we test by case-insensitive string match.
15851        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15852        {
15853            self.advance();
15854            if !matches!(self.peek(), Token::LParen) {
15855                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15856            }
15857            self.advance();
15858            let mut cols = Vec::new();
15859            loop {
15860                cols.push(self.expect_ident_like()?);
15861                match self.peek() {
15862                    Token::Comma => {
15863                        self.advance();
15864                    }
15865                    Token::RParen => {
15866                        self.advance();
15867                        break;
15868                    }
15869                    other => {
15870                        return Err(self.err(format!(
15871                            "expected ',' or ')' in INCLUDE list, got {other:?}"
15872                        )));
15873                    }
15874                }
15875            }
15876            cols
15877        } else {
15878            Vec::new()
15879        };
15880        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15881        // storage parameters. pgvector emits `WITH (lists = N)` for
15882        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15883        // SPG's HNSW picks its own parameters today (tunable via
15884        // env vars), so the WITH clause is informational and dropped.
15885        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15886            self.advance();
15887            if !matches!(self.peek(), Token::LParen) {
15888                return Err(self.err(format!(
15889                    "expected '(' after WITH in CREATE INDEX, got {:?}",
15890                    self.peek()
15891                )));
15892            }
15893            self.advance();
15894            loop {
15895                if matches!(self.peek(), Token::RParen) {
15896                    self.advance();
15897                    break;
15898                }
15899                // Drain `key = value` or bare `key` tokens.
15900                let _ = self.advance(); // key
15901                if matches!(self.peek(), Token::Eq) {
15902                    self.advance();
15903                    let _ = self.advance(); // value (int / string / ident)
15904                }
15905                match self.peek() {
15906                    Token::Comma => {
15907                        self.advance();
15908                    }
15909                    Token::RParen => {
15910                        self.advance();
15911                        break;
15912                    }
15913                    other => {
15914                        return Err(self.err(format!(
15915                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
15916                        )));
15917                    }
15918                }
15919            }
15920        }
15921        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15922        // which sits between the key list and the WHERE clause.
15923        let mut nulls_not_distinct = false;
15924        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15925            let n1 = self.tokens.get(self.pos + 1);
15926            let n2 = self.tokens.get(self.pos + 2);
15927            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15928                self.advance(); // NULLS
15929                self.advance(); // NOT
15930                self.advance(); // DISTINCT
15931                nulls_not_distinct = true;
15932            } else if matches!(n1, Some(Token::Distinct)) {
15933                self.advance(); // NULLS
15934                self.advance(); // DISTINCT
15935            }
15936        }
15937        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15938        let partial_predicate = if matches!(self.peek(), Token::Where) {
15939            self.advance();
15940            Some(self.parse_expr(0)?)
15941        } else {
15942            None
15943        };
15944        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15945        // sense: uniqueness over an ANN structure has no clean
15946        // semantics. Reject early. (BRIN UNIQUE is similarly
15947        // meaningless — block both.)
15948        if is_unique && !matches!(method, IndexMethod::BTree) {
15949            return Err(self.err(alloc::format!(
15950                "UNIQUE is only supported on BTree indexes, got USING {:?}",
15951                method
15952            )));
15953        }
15954        Ok(Statement::CreateIndex(CreateIndexStatement {
15955            concurrently,
15956            name,
15957            key_order,
15958            key_collation,
15959            table,
15960            column,
15961            nulls_not_distinct,
15962            method,
15963            if_not_exists,
15964            included_columns,
15965            partial_predicate,
15966            extra_columns: extra_columns.clone(),
15967            expression,
15968            is_unique,
15969            opclass,
15970            method_name,
15971        }))
15972    }
15973
15974    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15975    /// column-level `REFERENCES ...` clause. The trailing FK is
15976    /// normalised into table-level shape (single-element columns +
15977    /// parent_columns) so the engine sees one uniform constraint list.
15978    fn parse_column_def_with_fk(
15979        &mut self,
15980    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15981        let col = self.parse_column_def()?;
15982        // v7.39 (round 308, V29) — an explicitly named inline FK:
15983        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15984        // loop leaves this spelling intact precisely so the name can be
15985        // kept here; PG reports it in violation messages and matches it
15986        // in `SET CONSTRAINTS`.
15987        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15988        {
15989            self.advance();
15990            Some(self.expect_ident_like()?)
15991        } else {
15992            None
15993        };
15994        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15995        let inline_references = matches!(
15996            self.peek(),
15997            Token::Ident(s) if s.eq_ignore_ascii_case("references")
15998        );
15999        if !inline_references {
16000            return Ok((col, None));
16001        }
16002        let (
16003            parent_table,
16004            parent_columns,
16005            on_delete,
16006            on_update,
16007            match_type,
16008            deferrable,
16009            initially_deferred,
16010        ) = self.parse_references_tail(1)?;
16011        let fk = ForeignKeyConstraint {
16012            name: declared_name,
16013            columns: vec![col.name.clone()],
16014            parent_table,
16015            parent_columns,
16016            on_delete,
16017            on_update,
16018            match_type,
16019            deferrable,
16020            initially_deferred,
16021        };
16022        Ok((col, Some(fk)))
16023    }
16024
16025    /// v7.13.0 — parse a column type (consuming the type ident and
16026    /// any trailing parameters / `[]`), without surrounding column
16027    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16028    /// Returns the resolved `ColumnTypeName` plus implied
16029    /// `(auto_increment, not_null)` flags from PG SERIAL family
16030    /// shorthands — callers that don't expect those (ALTER COLUMN
16031    /// TYPE) can discard them.
16032    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16033        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16034        Ok(ty)
16035    }
16036
16037    #[allow(clippy::type_complexity)]
16038    fn parse_type_with_implied_flags(
16039        &mut self,
16040    ) -> Result<
16041        (
16042            ColumnTypeName,
16043            bool,
16044            bool,
16045            Option<String>,
16046            Collation,
16047            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16048            bool,
16049            // v7.39 (round 676) — the collation NAME as written, which the
16050            // `Collation` enum above cannot carry.
16051            Option<String>,
16052            bool,
16053            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16054            // list captured at type-parse time. None for all
16055            // non-ENUM types.
16056            Option<Vec<String>>,
16057            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16058            // list. Distinct from ENUM (subset semantics).
16059            Option<Vec<String>>,
16060            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16061            // width, lost when the type collapses to SmallInt / Int.
16062            Option<MysqlIntWidth>,
16063            // v7.39 (round 424) — declared fractional-seconds precision of a
16064            // MySQL temporal column (bare spelling = 0). None under PG.
16065            Option<u8>,
16066        ),
16067        ParseError,
16068    > {
16069        let mut ty_ident = match self.advance() {
16070            Token::Ident(s) => s,
16071            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16072            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16073            // '<span>'` literal grammar. As a column type it lands
16074            // here directly; downstream resolution still uses the
16075            // canonical lowercase string.
16076            Token::Interval => "interval".to_string(),
16077            other => {
16078                return Err(ParseError {
16079                    message: format!("expected column type, got {other:?}"),
16080                    token_pos: self.consumed_pos(),
16081                });
16082            }
16083        };
16084        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16085        // pg_dump qualifies extension types (`public.vector(1024)`).
16086        // SPG is single-namespace; drop the schema and resolve the
16087        // bare type — same treatment table names already get.
16088        while matches!(self.peek(), Token::Dot) {
16089            self.advance();
16090            ty_ident = self.expect_ident_like()?;
16091        }
16092        let mut implied_auto_increment = false;
16093        let mut implied_not_null = false;
16094        let mut user_type_ref: Option<String> = None;
16095        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16096        // value list, captured here and bubbled up through the
16097        // ColumnDef so the engine can attach it to the column
16098        // schema (and validate INSERT cells against it).
16099        let mut inline_enum_variants: Option<Vec<String>> = None;
16100        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16101        let mut inline_set_variants: Option<Vec<String>> = None;
16102        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16103        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16104        // collapses to SmallInt / Int. Only under the MySQL dialect.
16105        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16106        // v7.39 (round 424) — the declared fractional-seconds precision of a
16107        // MySQL temporal column. Set by the temporal arms below; stays None
16108        // for PG (whose temporal columns keep full microseconds).
16109        let mut mysql_fsp: Option<u8> = None;
16110        let mut ty = match ty_ident.as_str() {
16111            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16112            "smallserial" | "serial2" => {
16113                implied_auto_increment = true;
16114                implied_not_null = true;
16115                ColumnTypeName::SmallInt
16116            }
16117            "serial" | "serial4" => {
16118                implied_auto_increment = true;
16119                implied_not_null = true;
16120                ColumnTypeName::Int
16121            }
16122            "bigserial" | "serial8" => {
16123                implied_auto_increment = true;
16124                implied_not_null = true;
16125                ColumnTypeName::BigInt
16126            }
16127            // MySQL flavours we accept by aliasing to the closest SPG
16128            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16129            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16130            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16131            // without semantic effect.
16132            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16133            // PG's internal type names; pg_dump and hand-written PG schemas
16134            // use them interchangeably with smallint / int / bigint (the cast
16135            // path already accepted them, only the column grammar didn't).
16136            "smallint" | "int2" => {
16137                // v7.14.0 — MySQL display-width on integers
16138                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16139                // parenthesised number is purely cosmetic — it
16140                // doesn't change storage. Accept + discard.
16141                self.consume_optional_paren_size();
16142                ColumnTypeName::SmallInt
16143            }
16144            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16145            // canonical encoding for BOOLEAN. Every MySQL driver
16146            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16147            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16148            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16149            // gave the customer i16-shaped values where the app
16150            // expected bool — a Tier-A silent type drift on
16151            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16152            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16153            // stay SmallInt (the legacy width-agnostic path).
16154            "tinyint" => {
16155                let width = self.peek_optional_paren_size_value();
16156                self.consume_optional_paren_size();
16157                if width == Some(1) {
16158                    ColumnTypeName::Bool
16159                } else {
16160                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16161                    // lost width so the write path can enforce -128..127.
16162                    if self.mysql_dialect {
16163                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16164                    }
16165                    ColumnTypeName::SmallInt
16166                }
16167            }
16168            "mediumint" => {
16169                self.consume_optional_paren_size();
16170                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16171                if self.mysql_dialect {
16172                    mysql_int_width = Some(MysqlIntWidth::Medium);
16173                }
16174                ColumnTypeName::Int
16175            }
16176            "int" | "integer" | "int4" => {
16177                self.consume_optional_paren_size();
16178                ColumnTypeName::Int
16179            }
16180            "bigint" | "int8" => {
16181                self.consume_optional_paren_size();
16182                ColumnTypeName::BigInt
16183            }
16184            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16185            // (mailrs round-5 G6). Consume the optional `PRECISION`
16186            // tail when the type keyword was `double` / `DOUBLE`.
16187            //
16188            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16189            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16190            // p in 1..=24 is real, 25..=53 is double precision, and
16191            // anything else is an error.
16192            "float" | "double" | "real" => {
16193                if ty_ident.eq_ignore_ascii_case("double")
16194                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16195                {
16196                    self.advance();
16197                }
16198                if ty_ident.eq_ignore_ascii_case("real") {
16199                    // v7.39 (round 274) — the two dialects genuinely
16200                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16201                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16202                    // 32-bit globally and thereby narrowed the stored
16203                    // precision of every MySQL REAL column.
16204                    if self.mysql_dialect {
16205                        ColumnTypeName::Float
16206                    } else {
16207                        ColumnTypeName::Real
16208                    }
16209                } else if ty_ident.eq_ignore_ascii_case("float")
16210                    && self.mysql_dialect
16211                    && matches!(self.peek(), Token::LParen)
16212                    && self.peek_paren_has_comma()
16213                {
16214                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16215                    // display form (`FLOAT(10,2)`), which PG has no
16216                    // equivalent of. It was `syntax error at or near ","`,
16217                    // so the whole CREATE failed. The digits are a display
16218                    // hint only; SPG stores the full double.
16219                    self.consume_optional_paren_size();
16220                    ColumnTypeName::Float
16221                } else if ty_ident.eq_ignore_ascii_case("float")
16222                    && matches!(self.peek(), Token::LParen)
16223                {
16224                    // PG words the two bounds differently, and
16225                    // parse_paren_size already rejects a zero.
16226                    let p = self.parse_paren_size("FLOAT")?;
16227                    if p > 53 {
16228                        return Err(self.err(String::from(
16229                            "precision for type float must be less than 54 bits",
16230                        )));
16231                    }
16232                    if p <= 24 {
16233                        ColumnTypeName::Real
16234                    } else {
16235                        ColumnTypeName::Float
16236                    }
16237                } else {
16238                    ColumnTypeName::Float
16239                }
16240            }
16241            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16242            "float4" => ColumnTypeName::Real,
16243            "float8" => ColumnTypeName::Float,
16244            "text" => ColumnTypeName::Text,
16245            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16246            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16247            // real MySQL schema and NONE of them existed: the CREATE
16248            // failed outright with `type "blob" does not exist`, so the
16249            // table was never made. The sizes differ only in MySQL's
16250            // maximum length, which SPG does not cap, so they collapse
16251            // onto TEXT and BYTEA the way the unsized spellings do.
16252            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16253            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16254            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16255            // enforce, consumed so the declaration parses.
16256            "varbinary" | "binary" => {
16257                self.consume_optional_paren_size();
16258                ColumnTypeName::Bytes
16259            }
16260            "name" => ColumnTypeName::Name,
16261            "xid" => ColumnTypeName::Xid,
16262            "oid" => ColumnTypeName::Oid,
16263            "xid8" => ColumnTypeName::Xid8,
16264            "bool" | "boolean" => ColumnTypeName::Bool,
16265            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16266            // an unbounded `character varying`, which the arm below has always
16267            // read as text. Only the short spelling demanded a length, so
16268            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16269            // there is — failed on `VARCHAR type requires (N)` while the long
16270            // spelling of the same thing was accepted. The same asymmetry
16271            // round 613 closed on the CAST side, here on the DDL side.
16272            "varchar" => {
16273                if matches!(self.peek(), Token::LParen) {
16274                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16275                } else {
16276                    ColumnTypeName::Text
16277                }
16278            }
16279            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16280            // `character` below (SQL standard).
16281            "char" => {
16282                if matches!(self.peek(), Token::LParen) {
16283                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16284                } else {
16285                    ColumnTypeName::Char(1)
16286                }
16287            }
16288            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16289            // `character(n)` = char, bare `character` = char(1). Unbounded
16290            // `character varying` maps to text.
16291            "character" => {
16292                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16293                    self.advance();
16294                    if matches!(self.peek(), Token::LParen) {
16295                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16296                    } else {
16297                        ColumnTypeName::Text
16298                    }
16299                } else if matches!(self.peek(), Token::LParen) {
16300                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16301                } else {
16302                    ColumnTypeName::Char(1)
16303                }
16304            }
16305            "vector" => {
16306                let dim = self.parse_paren_size("VECTOR")?;
16307                let encoding = self.parse_optional_vector_encoding()?;
16308                ColumnTypeName::Vector { dim, encoding }
16309            }
16310            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16311            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16312            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16313            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16314            // DECIMAL(10,2))` — how nearly every money column is written,
16315            // in either dialect — was a syntax error and the table was
16316            // never created. `FIXED` is MySQL's alias alone, so it is
16317            // taken only in that dialect.
16318            "numeric" | "decimal" | "dec" => {
16319                let (precision, scale) = self.parse_optional_numeric_params()?;
16320                ColumnTypeName::Numeric(precision, scale)
16321            }
16322            "fixed" if self.mysql_dialect => {
16323                let (precision, scale) = self.parse_optional_numeric_params()?;
16324                ColumnTypeName::Numeric(precision, scale)
16325            }
16326            "date" => ColumnTypeName::Date,
16327            // MySQL's `DATETIME` is the same domain as standard
16328            // `TIMESTAMP` — accept both spellings.
16329            "timestamp" | "datetime" => {
16330                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16331                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16332                // TIME ZONE` clause, so consume it first.
16333                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16334                // (it truncates on write and pads on render), so capture it;
16335                // a bare spelling means precision 0 there. PG stores µs always
16336                // and keeps `None`.
16337                let n = self.take_optional_paren_size();
16338                if self.mysql_dialect {
16339                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16340                }
16341                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16342                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16343                // the full form. SPG canonicalises:
16344                //   - WITH TIME ZONE    → Timestamptz
16345                //   - WITHOUT TIME ZONE → Timestamp
16346                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16347                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16348                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16349                {
16350                    self.advance(); // WITH
16351                    self.advance(); // TIME
16352                    self.advance(); // ZONE
16353                    ColumnTypeName::Timestamptz
16354                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16355                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16356                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16357                {
16358                    self.advance(); // WITHOUT
16359                    self.advance(); // TIME
16360                    self.advance(); // ZONE
16361                    ColumnTypeName::Timestamp
16362                } else {
16363                    // A second `(precision)` cannot legally follow, but the
16364                    // old grammar tolerated it; keep that tolerance.
16365                    self.consume_optional_paren_size();
16366                    ColumnTypeName::Timestamp
16367                }
16368            }
16369            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16370            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16371            // only PG-wire OID differs.
16372            "timestamptz" => {
16373                self.consume_optional_paren_size();
16374                ColumnTypeName::Timestamptz
16375            }
16376            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16377            // validation. We accept the JSONB spelling too because
16378            // most PG clients default to it; SPG doesn't distinguish
16379            // the two (no path-operator perf advantage to model).
16380            "json" => ColumnTypeName::Json,
16381            "jsonb" => ColumnTypeName::Jsonb,
16382            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16383            // surface here. Same storage shape; mapping happens at
16384            // the engine side via the ColumnTypeName → DataType
16385            // resolver. Literal forms are handled at coerce_value
16386            // time so the lexer stays untouched.
16387            "bytea" | "bytes" => ColumnTypeName::Bytes,
16388            // v7.17.0 Phase 7 — PG network address types
16389            // v7.17.0 had a Text-backed fallback here for
16390            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16391            // each to a first-class type; the keywords are
16392            // bound below in the ζ-A block.
16393            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16394            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16395            // arrives in v7.12.1+; the type itself loads here so
16396            // mailrs's `scripts/init-schema.sql` runs unmodified.
16397            "tsvector" => ColumnTypeName::TsVector,
16398            "tsquery" => ColumnTypeName::TsQuery,
16399            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16400            // surface for Django / Rails / Hibernate's default
16401            // PK pattern.
16402            "uuid" => ColumnTypeName::Uuid,
16403            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16404            // Storage = three-field {months, days, micros}, catalog
16405            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16406            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16407            "interval" => {
16408                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16409                // SECOND` and an optional `(p)` precision. SPG stores the full
16410                // {months,days,micros}; consume + ignore the qualifier/precision.
16411                while matches!(self.peek(), Token::To)
16412                    || matches!(self.peek(), Token::Ident(s) if matches!(
16413                        s.to_ascii_lowercase().as_str(),
16414                        "year" | "month" | "day" | "hour" | "minute" | "second"
16415                    ))
16416                {
16417                    self.advance();
16418                }
16419                self.consume_optional_paren_size();
16420                ColumnTypeName::Interval
16421            }
16422            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16423            // i64 microseconds since 00:00:00. Wire OID 1083.
16424            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16425            "time" => {
16426                // v7.39 (round 424) — MySQL TIME carries a semantic
16427                // fractional-seconds precision, bare meaning 0.
16428                let n = self.take_optional_paren_size();
16429                if self.mysql_dialect {
16430                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16431                }
16432                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16433                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16434                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16435                {
16436                    self.advance();
16437                    self.advance();
16438                    self.advance();
16439                    ColumnTypeName::TimeTz
16440                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16441                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16442                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16443                {
16444                    self.advance();
16445                    self.advance();
16446                    self.advance();
16447                    ColumnTypeName::Time
16448                } else {
16449                    ColumnTypeName::Time
16450                }
16451            }
16452            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16453            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16454            "year" => ColumnTypeName::Year,
16455            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16456            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16457            "timetz" => ColumnTypeName::TimeTz,
16458            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16459            // Wire OID 790.
16460            "money" => ColumnTypeName::Money,
16461            // v7.17.0 Phase 3.P0-38 — PG range types.
16462            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16463            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16464            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16465            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16466            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16467            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16468            // v7.37.5 δ — PG 14+ multirange keywords.
16469            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16470            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16471            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16472            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16473            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16474            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16475            // v7.37.5 ε — PG geometry scalar keywords.
16476            "point" => ColumnTypeName::Point,
16477            "lseg" => ColumnTypeName::Lseg,
16478            "path" => ColumnTypeName::Path,
16479            "box" => ColumnTypeName::PgBox,
16480            "polygon" => ColumnTypeName::Polygon,
16481            "line" => ColumnTypeName::Line,
16482            "circle" => ColumnTypeName::Circle,
16483            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16484            "inet" => ColumnTypeName::Inet,
16485            "cidr" => ColumnTypeName::Cidr,
16486            "macaddr" => ColumnTypeName::Macaddr,
16487            "macaddr8" => ColumnTypeName::Macaddr8,
16488            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16489            // width in the value, so the optional `(N)` typmod is accepted and
16490            // ignored (the column stores whatever width it's given).
16491            "bit" => {
16492                let varying = matches!(
16493                    self.peek(),
16494                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16495                );
16496                if varying {
16497                    self.advance();
16498                }
16499                // v7.39 (round 281) — the length used to be parsed and
16500                // dropped, so `bit(3)` accepted a five-bit string.
16501                let n = if matches!(self.peek(), Token::LParen) {
16502                    self.parse_paren_size("BIT")?
16503                } else {
16504                    0
16505                };
16506                if varying {
16507                    ColumnTypeName::BitVarying(n)
16508                } else {
16509                    ColumnTypeName::Bit(n)
16510                }
16511            }
16512            "varbit" => {
16513                let n = if matches!(self.peek(), Token::LParen) {
16514                    self.parse_paren_size("VARBIT")?
16515                } else {
16516                    0
16517                };
16518                ColumnTypeName::BitVarying(n)
16519            }
16520            "xml" => ColumnTypeName::Xml,
16521            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16522            "hstore" => ColumnTypeName::Hstore,
16523            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16524            // `ENUM('a','b','c')`. Storage is TEXT; the value
16525            // list lands on `inline_enum_variants` for the
16526            // engine to validate INSERT cells against. Empty
16527            // value list is a parse error (matches MySQL).
16528            "enum" => {
16529                // Expect the opening `(`.
16530                if !matches!(self.peek(), Token::LParen) {
16531                    return Err(self.err(alloc::format!(
16532                        "expected '(' after ENUM, got {:?}",
16533                        self.peek()
16534                    )));
16535                }
16536                self.advance();
16537                let mut variants: Vec<String> = Vec::new();
16538                loop {
16539                    match self.advance() {
16540                        Token::String(s) => variants.push(s),
16541                        other => {
16542                            return Err(self.err(alloc::format!(
16543                                "ENUM(...) expects string literal variants, got {other:?}"
16544                            )));
16545                        }
16546                    }
16547                    match self.peek() {
16548                        Token::Comma => {
16549                            self.advance();
16550                            continue;
16551                        }
16552                        Token::RParen => {
16553                            self.advance();
16554                            break;
16555                        }
16556                        other => {
16557                            return Err(self.err(alloc::format!(
16558                                "expected ',' or ')' in ENUM(...), got {other:?}"
16559                            )));
16560                        }
16561                    }
16562                }
16563                if variants.is_empty() {
16564                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16565                }
16566                inline_enum_variants = Some(variants);
16567                // Storage is plain TEXT; the variant list lives on
16568                // the ColumnSchema side.
16569                ColumnTypeName::Text
16570            }
16571            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16572            // `SET('a','b','c')`. Same parse shape as ENUM;
16573            // semantics differ (subset rather than pick-one).
16574            "set" => {
16575                if !matches!(self.peek(), Token::LParen) {
16576                    return Err(self.err(alloc::format!(
16577                        "expected '(' after SET, got {:?}",
16578                        self.peek()
16579                    )));
16580                }
16581                self.advance();
16582                let mut variants: Vec<String> = Vec::new();
16583                loop {
16584                    match self.advance() {
16585                        Token::String(s) => variants.push(s),
16586                        other => {
16587                            return Err(self.err(alloc::format!(
16588                                "SET(...) expects string literal variants, got {other:?}"
16589                            )));
16590                        }
16591                    }
16592                    match self.peek() {
16593                        Token::Comma => {
16594                            self.advance();
16595                            continue;
16596                        }
16597                        Token::RParen => {
16598                            self.advance();
16599                            break;
16600                        }
16601                        other => {
16602                            return Err(self.err(alloc::format!(
16603                                "expected ',' or ')' in SET(...), got {other:?}"
16604                            )));
16605                        }
16606                    }
16607                }
16608                if variants.is_empty() {
16609                    return Err(self.err("SET(...) must declare at least one variant".into()));
16610                }
16611                inline_set_variants = Some(variants);
16612                ColumnTypeName::Text
16613            }
16614            _other => {
16615                // v7.17.0 Phase 1.4 — unknown ident → defer
16616                // resolution to the engine. Stored as Text in
16617                // ColumnTypeName + the original name carried as
16618                // `user_type_ref` so CREATE TABLE can look up
16619                // user-defined enum / domain types.
16620                user_type_ref = Some(ty_ident.clone());
16621                ColumnTypeName::Text
16622            }
16623        };
16624        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16625        // right after the type keyword. Pre-4.4 SPG consumed +
16626        // discarded the keyword, leaving a customer column
16627        // declared `id INT UNSIGNED NOT NULL` silently accepting
16628        // negative values — a Tier-A correctness drift where
16629        // application invariants (auto-increment-IDs never
16630        // negative) silently broke on cutover. Now: capture as
16631        // a column flag, persist on the schema, enforce at
16632        // INSERT / UPDATE time.
16633        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16634        {
16635            self.advance();
16636            true
16637        } else {
16638            false
16639        };
16640        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16641        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16642        // stores text as UTF-8 always so CHARACTER SET is still a
16643        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16644        // name: it gets classified into a `Collation` variant the
16645        // engine consults at WHERE-eval time. PG `default` /
16646        // `pg_catalog.default` / `C` / `POSIX` collations all
16647        // resolve to `Binary` (the prior behaviour); `_ci` /
16648        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16649        // The schema-qualifier form (`pg_catalog.default`) lexes
16650        // as `Ident '.' Ident` — peek for the `.` and consume both
16651        // halves so it's treated as one collation name. PG's
16652        // `IDENT.IDENT` collation form (which can appear here) is
16653        // resolved by Collation::from_collation_name on the bare
16654        // identifier after the dot.
16655        let mut collation = Collation::Binary;
16656        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16657        // clause was written. The engine needs this to tell an explicit
16658        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16659        // clause at all: both resolve to `Collation::Binary`, but under the
16660        // MySQL dialect the latter takes the folding default collation.
16661        let mut collation_explicit = false;
16662        let mut collation_name: Option<alloc::string::String> = None;
16663        loop {
16664            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16665                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16666            {
16667                self.advance(); // CHARACTER
16668                self.advance(); // SET
16669                if matches!(
16670                    self.peek(),
16671                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16672                ) {
16673                    self.advance();
16674                }
16675                continue;
16676            }
16677            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16678                self.advance(); // COLLATE
16679                // Accept Ident / QuotedIdent / String AND the
16680                // keyword-tokenised `Default` (PG `pg_catalog.default`
16681                // and bare `DEFAULT` collation names — `default` is a
16682                // reserved word so the lexer hands back Token::Default
16683                // not Token::Ident).
16684                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16685                    match this.peek().clone() {
16686                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16687                            this.advance();
16688                            Some(s)
16689                        }
16690                        Token::Default => {
16691                            this.advance();
16692                            Some(alloc::string::String::from("default"))
16693                        }
16694                        _ => None,
16695                    }
16696                };
16697                let raw = if let Some(head) = read_collation_atom(self) {
16698                    // Schema-qualified PG form: `pg_catalog.default`.
16699                    if matches!(self.peek(), Token::Dot) {
16700                        self.advance();
16701                        let tail = read_collation_atom(self).unwrap_or_default();
16702                        alloc::format!("{head}.{tail}")
16703                    } else {
16704                        head
16705                    }
16706                } else {
16707                    alloc::string::String::new()
16708                };
16709                if !raw.is_empty() {
16710                    collation_explicit = true;
16711                    // v7.39 (round 676) — keep the name too. The enum below
16712                    // folds C / POSIX / en_US / default into one value, and
16713                    // `pg_attribute.attcollation` has to tell them apart.
16714                    // The schema qualifier goes: PG's `pg_catalog.default`
16715                    // and a bare `default` name the same collation.
16716                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16717                    // encoding suffix. Round 676 used `rsplit('.')` for
16718                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16719                    // PG writes `pg_catalog.default` (qualifier) and
16720                    // `en_US.utf8` (locale + encoding) with the same
16721                    // separator. Only `pg_catalog.` is a qualifier, and it
16722                    // is the only one PG's own dumps emit.
16723                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16724                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16725                    collation_name = Some(alloc::string::String::from(bare));
16726                    let parsed = Collation::from_collation_name(&raw);
16727                    // Last COLLATE clause wins, but `Binary` from a
16728                    // bare keyword like `default` should not
16729                    // silently downgrade a stronger one set earlier
16730                    // on the same column. v7.17 only ships one
16731                    // non-Binary variant so a simple OR is enough.
16732                    if parsed != Collation::Binary {
16733                        collation = parsed;
16734                    }
16735                }
16736                continue;
16737            }
16738            break;
16739        }
16740        // v7.10.10 — postfix `[]` widens the base type to its array
16741        // type. PG accepts `TYPE[]` after any base type and so does
16742        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16743        // all through; the old "only TEXT[]" note was stale).
16744        if matches!(self.peek(), Token::LBracket) {
16745            self.advance();
16746            if !matches!(self.peek(), Token::RBracket) {
16747                return Err(self.err(alloc::format!(
16748                    "TEXT[] takes no dimension; got {:?}",
16749                    self.peek()
16750                )));
16751            }
16752            self.advance();
16753            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16754            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16755            // still error here.
16756            ty = match ty {
16757                ColumnTypeName::Text => ColumnTypeName::TextArray,
16758                ColumnTypeName::Int => ColumnTypeName::IntArray,
16759                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16760                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16761                // `[]` grammar. Wire OID 1187.
16762                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16763                // v7.37.5 γ — full PG array-of-scalar family.
16764                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16765                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16766                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16767                // NUMERIC(p, s) loses its precision params at the
16768                // array level (matches PG: `NUMERIC[]` is untyped,
16769                // per-element precision flows through values).
16770                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16771                ColumnTypeName::Date => ColumnTypeName::DateArray,
16772                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16773                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16774                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16775                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16776                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16777                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16778                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16779                // the array level (matches PG semantics where the
16780                // element precision is per-row, not column-wide).
16781                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16782                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16783                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16784                // follow-up.
16785                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16786                other => {
16787                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16788                }
16789            };
16790            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16791            // for INT/TEXT/BIGINT. Anything else is an error.
16792            if matches!(self.peek(), Token::LBracket) {
16793                self.advance();
16794                if !matches!(self.peek(), Token::RBracket) {
16795                    return Err(self.err(alloc::format!(
16796                        "TYPE[][] second dimension takes no size; got {:?}",
16797                        self.peek()
16798                    )));
16799                }
16800                self.advance();
16801                ty = match ty {
16802                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16803                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16804                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16805                    // v7.39 (read01 round 75) — bool[][].
16806                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16807                    other => {
16808                        return Err(self.err(alloc::format!(
16809                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16810                             TEXT[][] only; got {other:?}"
16811                        )));
16812                    }
16813                };
16814            }
16815        }
16816        Ok((
16817            ty,
16818            implied_auto_increment,
16819            implied_not_null,
16820            user_type_ref,
16821            collation,
16822            collation_explicit,
16823            collation_name,
16824            is_unsigned,
16825            inline_enum_variants,
16826            inline_set_variants,
16827            mysql_int_width,
16828            mysql_fsp,
16829        ))
16830    }
16831
16832    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16833        // v7.20 — PG reserves the table-constraint keywords, so a
16834        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16835        // malformed constraint clause (e.g. `UNIQUE a` missing its
16836        // parens), not a column named "unique". Since v7.17's
16837        // unknown-type leniency (`user_type_ref`) such a clause
16838        // would otherwise parse as a column with a user-defined
16839        // type — silently accepting invalid DDL. Quoted
16840        // identifiers ("unique" / `unique`) remain valid names.
16841        if let Token::Ident(s) = self.peek()
16842            && [
16843                "unique",
16844                "primary",
16845                "foreign",
16846                "constraint",
16847                "check",
16848                "references",
16849                "exclude",
16850            ]
16851            .iter()
16852            .any(|kw| s.eq_ignore_ascii_case(kw))
16853        {
16854            return Err(self.err(alloc::format!(
16855                "unexpected reserved keyword '{s}' at start of column definition \
16856                 (malformed table constraint?)"
16857            )));
16858        }
16859        let name = self.expect_ident_like()?;
16860        let (
16861            ty,
16862            implied_auto_increment,
16863            implied_not_null,
16864            user_type_ref,
16865            collation,
16866            collation_explicit,
16867            collation_name,
16868            is_unsigned,
16869            inline_enum_variants,
16870            inline_set_variants,
16871            mysql_int_width,
16872            mysql_fsp,
16873        ) = self.parse_type_with_implied_flags()?;
16874        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16875        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16876        // each at most once.
16877        let mut default: Option<Expr> = None;
16878        let mut nullable = !implied_not_null;
16879        let mut nullability_seen = implied_not_null;
16880        let mut auto_increment = implied_auto_increment;
16881        let mut is_primary_key = false;
16882        let mut is_unique = false;
16883        let mut unique_nulls_not_distinct = false;
16884        let mut constraint_deferrable = false;
16885        let mut constraint_initially_deferred = false;
16886        let mut check: Option<Expr> = None;
16887        let mut on_update_runtime: Option<Expr> = None;
16888        let mut generated_stored_expr: Option<Box<Expr>> = None;
16889        let mut identity_always = false;
16890        loop {
16891            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16892            // not-null constraints by name and pg_dump emits them
16893            // inline: `id bigint CONSTRAINT contacts_id_not_null1
16894            // NOT NULL`. Accept and discard the name; whatever
16895            // constraint follows is parsed by the arms below.
16896            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16897                // v7.39 (round 308, V29) — a name on an inline
16898                // REFERENCES belongs to the FOREIGN KEY, and the caller
16899                // (`parse_column_def_with_fk`) is what builds it, so
16900                // leave the whole clause for it. Dropping the name here
16901                // is what made `CONSTRAINT fk_a REFERENCES …` come back
16902                // as the synthesised `c_pid_fkey` — which then could
16903                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16904                // `advance()` takes tokens by `mem::replace`, so there
16905                // is no rewinding once consumed.
16906                if matches!(
16907                    self.tokens.get(self.pos + 2),
16908                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16909                ) {
16910                    break;
16911                }
16912                self.advance();
16913                let _name = self.expect_ident_like()?;
16914                continue;
16915            }
16916            // v7.39 (round 379) — MySQL's SHORT generated-column form
16917            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16918            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16919            // below), but hand-written schemas and app migrations use this.
16920            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16921            // SPG computes-and-stores either way, like the long form.
16922            if matches!(self.peek(), Token::As) {
16923                self.advance();
16924                if !matches!(self.peek(), Token::LParen) {
16925                    return Err(self.err(alloc::format!(
16926                        "expected '(' after AS in a generated column, got {:?}",
16927                        self.peek()
16928                    )));
16929                }
16930                self.advance();
16931                let expr = self.parse_expr(0)?;
16932                if !matches!(self.peek(), Token::RParen) {
16933                    return Err(self.err(alloc::format!(
16934                        "expected ')' after AS (<expr>), got {:?}",
16935                        self.peek()
16936                    )));
16937                }
16938                self.advance();
16939                if matches!(self.peek(), Token::Ident(s)
16940                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16941                {
16942                    self.advance();
16943                }
16944                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16945                continue;
16946            }
16947            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16948            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16949            // the modern replacement for SERIAL in hand-written
16950            // schemas). Both flavours map onto the auto-increment
16951            // machinery — SPG's serial semantics ≈ BY DEFAULT;
16952            // ALWAYS's reject-explicit-values nuance is documented
16953            // leniency. Generated EXPRESSION columns
16954            // (`AS (expr) STORED`) are not supported: error loudly
16955            // instead of silently storing NULLs.
16956            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16957                self.advance();
16958                let mut saw_generated_always = false;
16959                match self.peek().clone() {
16960                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16961                        self.advance();
16962                        saw_generated_always = true;
16963                    }
16964                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16965                        self.advance();
16966                        if !matches!(self.peek(), Token::Default) {
16967                            return Err(self.err(alloc::format!(
16968                                "expected DEFAULT after GENERATED BY, got {:?}",
16969                                self.peek()
16970                            )));
16971                        }
16972                        self.advance();
16973                    }
16974                    other => {
16975                        return Err(self.err(alloc::format!(
16976                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16977                        )));
16978                    }
16979                }
16980                if !matches!(self.peek(), Token::As) {
16981                    return Err(self.err(alloc::format!(
16982                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16983                        self.peek()
16984                    )));
16985                }
16986                self.advance();
16987                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16988                // ( <expr> ) STORED` stored computed-column. The
16989                // expression is captured for the engine to recompute
16990                // on every INSERT / UPDATE. v7.37.7 accepts the
16991                // STORED keyword only; PG also has VIRTUAL, which
16992                // v7.37.7 carves out (sentori only uses STORED).
16993                if matches!(self.peek(), Token::LParen) {
16994                    self.advance();
16995                    let expr = self.parse_expr(0)?;
16996                    if !matches!(self.peek(), Token::RParen) {
16997                        return Err(self.err(alloc::format!(
16998                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16999                            self.peek()
17000                        )));
17001                    }
17002                    self.advance();
17003                    let stored = match self.peek() {
17004                        Token::Ident(s) | Token::QuotedIdent(s)
17005                            if s.eq_ignore_ascii_case("stored") =>
17006                        {
17007                            self.advance();
17008                            true
17009                        }
17010                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17011                        // generated columns. SPG computes them on write and
17012                        // persists like STORED; the two are observably
17013                        // identical for query results (the value, recompute
17014                        // on base-column change, and NOT NULL enforcement all
17015                        // match), so a PG 18 schema/dump using VIRTUAL loads
17016                        // and behaves correctly. The compute-on-read storage
17017                        // saving is an invisible internal difference.
17018                        Token::Ident(s) | Token::QuotedIdent(s)
17019                            if s.eq_ignore_ascii_case("virtual") =>
17020                        {
17021                            self.advance();
17022                            false
17023                        }
17024                        other => {
17025                            return Err(self.err(alloc::format!(
17026                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17027                                 got {other:?}"
17028                            )));
17029                        }
17030                    };
17031                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17032                    generated_stored_expr = Some(Box::new(expr));
17033                    continue;
17034                }
17035                self.expect_keyword_ident("identity")?;
17036                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17037                // consume the balanced parens and discard (SPG's
17038                // auto-increment is max+1-scan based).
17039                if matches!(self.peek(), Token::LParen) {
17040                    let mut depth = 0usize;
17041                    loop {
17042                        match self.advance() {
17043                            Token::LParen => depth += 1,
17044                            Token::RParen => {
17045                                depth -= 1;
17046                                if depth == 0 {
17047                                    break;
17048                                }
17049                            }
17050                            Token::Eof => {
17051                                return Err(self.err(
17052                                    "unterminated sequence-options parens after IDENTITY".into(),
17053                                ));
17054                            }
17055                            _ => {}
17056                        }
17057                    }
17058                }
17059                auto_increment = true;
17060                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17061                // can reject explicit non-DEFAULT INSERT values (unless
17062                // OVERRIDING SYSTEM VALUE) the way PG does.
17063                identity_always = saw_generated_always;
17064                // PG identity columns are implicitly NOT NULL.
17065                nullable = false;
17066                continue;
17067            }
17068            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17069            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17070            // is accepted today. The "ON" token is an Ident
17071            // (not reserved) — peek before consuming.
17072            if matches!(self.peek(), Token::On)
17073                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17074            {
17075                self.advance(); // ON
17076                self.advance(); // update
17077                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17078                let next = self.peek().clone();
17079                match next {
17080                    Token::Ident(s) | Token::QuotedIdent(s)
17081                        if s.eq_ignore_ascii_case("current_timestamp") =>
17082                    {
17083                        self.advance();
17084                        // Optional `(N)` precision.
17085                        if matches!(self.peek(), Token::LParen) {
17086                            self.advance();
17087                            if !matches!(self.peek(), Token::Integer(_)) {
17088                                return Err(self.err(alloc::format!(
17089                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17090                                    self.peek()
17091                                )));
17092                            }
17093                            self.advance();
17094                            if !matches!(self.peek(), Token::RParen) {
17095                                return Err(self.err(alloc::format!(
17096                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17097                                    self.peek()
17098                                )));
17099                            }
17100                            self.advance();
17101                        }
17102                        on_update_runtime = Some(Expr::FunctionCall {
17103                            name: "now".into(),
17104                            args: Vec::new(),
17105                        });
17106                        continue;
17107                    }
17108                    other => {
17109                        return Err(self.err(alloc::format!(
17110                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17111                        )));
17112                    }
17113                }
17114            }
17115            if matches!(self.peek(), Token::Default) {
17116                if default.is_some() {
17117                    return Err(self.err("DEFAULT specified twice".into()));
17118                }
17119                self.advance();
17120                default = Some(self.parse_expr(0)?);
17121                continue;
17122            }
17123            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17124            // token with NOT NULL and sits EARLIER in the loop than the
17125            // deferrability arm, so without the lookahead it was reported as
17126            // "NOT NULL specified twice" (or "expected NULL after NOT").
17127            if matches!(self.peek(), Token::Not)
17128                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17129            {
17130                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17131                self.consume_optional_deferrable_clauses()?;
17132                continue;
17133            }
17134            if matches!(self.peek(), Token::Not) {
17135                if nullability_seen {
17136                    return Err(self.err("NOT NULL specified twice".into()));
17137                }
17138                self.advance();
17139                if !matches!(self.peek(), Token::Null) {
17140                    return Err(self.err(format!(
17141                        "expected NULL after NOT in column def, got {:?}",
17142                        self.peek()
17143                    )));
17144                }
17145                self.advance();
17146                nullable = false;
17147                nullability_seen = true;
17148                continue;
17149            }
17150            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17151            // "this column is nullable" marker (the default in
17152            // standard SQL anyway). mysqldump emits it routinely
17153            // (`col TYPE NULL DEFAULT NULL` for nullable
17154            // timestamps etc). Accept + no-op.
17155            if matches!(self.peek(), Token::Null) {
17156                if nullability_seen && !nullable {
17157                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17158                    // sentence, PG18-measured (the table name is the
17159                    // caller's; the column half is exact).
17160                    return Err(self.err(alloc::format!(
17161                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17162                    )));
17163                }
17164                self.advance();
17165                nullable = true;
17166                nullability_seen = true;
17167                continue;
17168            }
17169            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17170            // arrives as a bare Ident. Match either, case-insensitive.
17171            if let Token::Ident(s) = self.peek()
17172                && (s.eq_ignore_ascii_case("auto_increment")
17173                    || s.eq_ignore_ascii_case("autoincrement"))
17174            {
17175                if auto_increment {
17176                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17177                }
17178                self.advance();
17179                auto_increment = true;
17180                continue;
17181            }
17182            // v7.9.13 — inline `PRIMARY KEY` column constraint
17183            // (mailrs F1). Implies `NOT NULL`. The engine creates
17184            // a BTree index for the PK column at CREATE TABLE time
17185            // so FK parent-side index lookups resolve.
17186            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17187            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17188            // spelling was a parse error, so a pg_dump carrying one stopped
17189            // mid-restore. The clauses are consumed by the same helper the FK
17190            // path has used since round 288 and recorded nowhere: SPG enforces
17191            // the constraint IMMEDIATELY either way, which fails earlier than
17192            // PG inside a transaction that violates-then-repairs — a refusal,
17193            // not a wrong answer. True deferral is the open remainder of F08.
17194            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17195                || (matches!(self.peek(), Token::Not)
17196                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17197            {
17198                // v7.39 (round 711) — CARRIED now (the storing half of
17199                // F08); round 621 only consumed.
17200                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17201                constraint_deferrable |= d;
17202                constraint_initially_deferred |= idef;
17203                continue;
17204            }
17205            if let Token::Ident(s) = self.peek()
17206                && s.eq_ignore_ascii_case("primary")
17207            {
17208                if is_primary_key {
17209                    return Err(self.err("PRIMARY KEY specified twice".into()));
17210                }
17211                // Peek-ahead for the required `KEY` token.
17212                let next = self.tokens.get(self.pos + 1);
17213                let next_is_key = matches!(
17214                    next,
17215                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17216                );
17217                if !next_is_key {
17218                    return Err(self.err(format!(
17219                        "expected KEY after PRIMARY in column def, got {:?}",
17220                        next
17221                    )));
17222                }
17223                self.advance(); // PRIMARY
17224                self.advance(); // KEY
17225                is_primary_key = true;
17226                if nullability_seen && nullable {
17227                    return Err(self.err(
17228                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17229                    ));
17230                }
17231                nullable = false;
17232                nullability_seen = true;
17233                continue;
17234            }
17235            // v7.13.0 — inline `UNIQUE` column constraint
17236            // (mailrs round-5 G2). Fold into a single-column
17237            // table-level UNIQUE at CREATE TABLE post-process time.
17238            if let Token::Ident(s) = self.peek()
17239                && s.eq_ignore_ascii_case("unique")
17240            {
17241                if is_unique {
17242                    return Err(self.err("UNIQUE specified twice".into()));
17243                }
17244                self.advance();
17245                is_unique = true;
17246                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17247                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17248                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17249                    let n1 = self.tokens.get(self.pos + 1);
17250                    let n2 = self.tokens.get(self.pos + 2);
17251                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17252                        self.advance(); // NULLS
17253                        self.advance(); // NOT
17254                        self.advance(); // DISTINCT
17255                        unique_nulls_not_distinct = true;
17256                    } else if matches!(n1, Some(Token::Distinct)) {
17257                        self.advance(); // NULLS
17258                        self.advance(); // DISTINCT
17259                    }
17260                }
17261                continue;
17262            }
17263            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17264            // (mailrs round-5 G3). PG semantics: column-level
17265            // CHECK is equivalent to a table-level CHECK. Multiple
17266            // inline CHECKs on the same column AND together.
17267            if let Token::Ident(s) = self.peek()
17268                && s.eq_ignore_ascii_case("check")
17269            {
17270                self.advance();
17271                if !matches!(self.peek(), Token::LParen) {
17272                    return Err(self.err(alloc::format!(
17273                        "expected '(' after CHECK in column def, got {:?}",
17274                        self.peek()
17275                    )));
17276                }
17277                self.advance();
17278                let pred = self.parse_expr(0)?;
17279                if !matches!(self.peek(), Token::RParen) {
17280                    return Err(self.err(alloc::format!(
17281                        "expected ')' to close CHECK predicate, got {:?}",
17282                        self.peek()
17283                    )));
17284                }
17285                self.advance();
17286                check = Some(match check.take() {
17287                    Some(prev) => Expr::Binary {
17288                        op: BinOp::And,
17289                        lhs: Box::new(prev),
17290                        rhs: Box::new(pred),
17291                    },
17292                    None => pred,
17293                });
17294                continue;
17295            }
17296            break;
17297        }
17298        Ok(ColumnDef {
17299            name,
17300            ty,
17301            nullable,
17302            default,
17303            auto_increment,
17304            is_primary_key,
17305            is_unique,
17306            unique_nulls_not_distinct,
17307            constraint_deferrable,
17308            constraint_initially_deferred,
17309            check,
17310            user_type_ref,
17311            on_update_runtime,
17312            collation,
17313            collation_explicit,
17314            collation_name,
17315            is_unsigned,
17316            inline_enum_variants,
17317            inline_set_variants,
17318            generated_stored_expr,
17319            identity_always,
17320            mysql_int_width,
17321            mysql_fsp,
17322        })
17323    }
17324
17325    /// `NUMERIC` may appear without parameters, with one (precision
17326    /// only, scale=0), or with both. Returns `(precision, scale)` with
17327    /// 0 = unspecified for the bare form.
17328    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17329        if !matches!(self.peek(), Token::LParen) {
17330            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17331            // we surface it as precision=0 to mean "unconstrained" so
17332            // the engine doesn't need a separate variant.
17333            return Ok((0, 0));
17334        }
17335        self.advance();
17336        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17337        // it words the out-of-range case with the value it saw. SPG
17338        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17339        // accepts failed to parse at all; values wider than i128 are
17340        // carried by the arbitrary-precision form.
17341        let precision = match self.advance() {
17342            Token::Integer(n) if (1..=1000).contains(&n) => {
17343                u16::try_from(n).expect("range-checked")
17344            }
17345            Token::Integer(n) => {
17346                return Err(ParseError {
17347                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17348                    token_pos: self.consumed_pos(),
17349                });
17350            }
17351            other => {
17352                return Err(ParseError {
17353                    message: format!(
17354                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17355                    ),
17356                    token_pos: self.consumed_pos(),
17357                });
17358            }
17359        };
17360        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17361        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17362        // then overflows). A negative scale rounds to tens / hundreds / …
17363        let scale = if matches!(self.peek(), Token::Comma) {
17364            self.advance();
17365            let neg = if matches!(self.peek(), Token::Minus) {
17366                self.advance();
17367                true
17368            } else {
17369                false
17370            };
17371            match self.advance() {
17372                Token::Integer(n) => {
17373                    let signed = if neg { -n } else { n };
17374                    if !(-1000..=1000).contains(&signed) {
17375                        return Err(ParseError {
17376                            message: format!(
17377                                "NUMERIC scale {signed} must be between -1000 and 1000"
17378                            ),
17379                            token_pos: self.consumed_pos(),
17380                        });
17381                    }
17382                    i16::try_from(signed).expect("range-checked")
17383                }
17384                other => {
17385                    return Err(ParseError {
17386                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17387                        token_pos: self.consumed_pos(),
17388                    });
17389                }
17390            }
17391        } else {
17392            0
17393        };
17394        if !matches!(self.peek(), Token::RParen) {
17395            return Err(self.err(format!(
17396                "expected ')' to close NUMERIC params, got {:?}",
17397                self.peek()
17398            )));
17399        }
17400        self.advance();
17401        Ok((precision, scale))
17402    }
17403
17404    /// Parse `(N)` where `N` is a positive integer literal — used by the
17405    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17406    /// for the error message.
17407    /// v6.0.1: parse the optional `USING <encoding>` clause that
17408    /// follows `VECTOR(N)` in a column definition. Missing clause
17409    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17410    /// ident → `ParseError` listing the encodings recognised today.
17411    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17412        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17413            return Ok(VecEncoding::F32);
17414        }
17415        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17416        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17417        // consume the token when the very next token is a known
17418        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17419        // `USING` for the caller — it's the rewrite-expression form.
17420        let n1 = self.tokens.get(self.pos + 1);
17421        let next_is_encoding = matches!(
17422            n1,
17423            Some(Token::Ident(s))
17424                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17425        );
17426        if !next_is_encoding {
17427            return Ok(VecEncoding::F32);
17428        }
17429        self.advance();
17430        let enc_ident = match self.advance() {
17431            Token::Ident(s) => s,
17432            other => {
17433                return Err(self.err(format!(
17434                    "expected vector encoding after USING, got {other:?}"
17435                )));
17436            }
17437        };
17438        match enc_ident.to_ascii_lowercase().as_str() {
17439            "sq8" => Ok(VecEncoding::Sq8),
17440            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17441            // binary16 per-element storage.
17442            "half" => Ok(VecEncoding::F16),
17443            other => Err(self.err(format!(
17444                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17445            ))),
17446        }
17447    }
17448
17449    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17450    /// without consuming it. Returns `Some(N)` when the next
17451    /// tokens are `( <int> )`; None otherwise. Used by the
17452    /// TINYINT classifier to decide whether to map to Bool or
17453    /// SmallInt.
17454    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17455        if !matches!(self.peek(), Token::LParen) {
17456            return None;
17457        }
17458        let next = self.tokens.get(self.pos + 1)?;
17459        let n = match next {
17460            Token::Integer(n) => *n,
17461            _ => return None,
17462        };
17463        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17464            return None;
17465        }
17466        Some(n)
17467    }
17468
17469    /// v7.14.0 — consume an optional MySQL display-width
17470    /// parenthesised number after an integer type, returning
17471    /// nothing. `TINYINT(1)` etc.
17472    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17473    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17474    fn peek_paren_has_comma(&self) -> bool {
17475        let mut i = self.pos + 1;
17476        let mut depth = 1usize;
17477        while depth > 0 {
17478            match self.tokens.get(i) {
17479                Some(Token::LParen) => depth += 1,
17480                Some(Token::RParen) => depth -= 1,
17481                Some(Token::Comma) if depth == 1 => return true,
17482                None | Some(Token::Eof) => return false,
17483                _ => {}
17484            }
17485            i += 1;
17486        }
17487        false
17488    }
17489
17490    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17491    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17492    /// fractional-seconds precision that drives write truncation and render
17493    /// padding, where `consume_optional_paren_size` throws it away.
17494    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17495    fn take_optional_paren_size(&mut self) -> Option<u8> {
17496        let Some(Token::Integer(n)) = self
17497            .tokens
17498            .get(self.pos + 1)
17499            .filter(|_| matches!(self.peek(), Token::LParen))
17500            .cloned()
17501        else {
17502            self.consume_optional_paren_size();
17503            return None;
17504        };
17505        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17506            self.consume_optional_paren_size();
17507            return None;
17508        }
17509        self.consume_optional_paren_size();
17510        u8::try_from(n).ok()
17511    }
17512
17513    fn consume_optional_paren_size(&mut self) {
17514        if !matches!(self.peek(), Token::LParen) {
17515            return;
17516        }
17517        self.advance();
17518        // Skip until matching RParen (allow nested or any tokens).
17519        let mut depth = 1usize;
17520        while depth > 0 {
17521            match self.peek() {
17522                Token::LParen => depth += 1,
17523                Token::RParen => depth -= 1,
17524                Token::Eof => return,
17525                _ => {}
17526            }
17527            self.advance();
17528        }
17529    }
17530
17531    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17532        if !matches!(self.peek(), Token::LParen) {
17533            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17534        }
17535        self.advance();
17536        let n = match self.advance() {
17537            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17538                message: format!("{label} size too large: {n}"),
17539                token_pos: self.consumed_pos(),
17540            })?,
17541            other => {
17542                return Err(ParseError {
17543                    message: format!("expected positive integer {label} size, got {other:?}"),
17544                    token_pos: self.consumed_pos(),
17545                });
17546            }
17547        };
17548        if !matches!(self.peek(), Token::RParen) {
17549            return Err(self.err(format!(
17550                "expected ')' after {label} size, got {:?}",
17551                self.peek()
17552            )));
17553        }
17554        self.advance();
17555        Ok(n)
17556    }
17557
17558    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17559    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17560    /// key, like MySQL) whose action skips conflicting rows.
17561    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17562    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17563    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17564    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17565    /// common bulk-upsert spellings —
17566    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17567    ///     REPLACE INTO t SELECT …
17568    /// — were a parse error / a duplicate-key failure respectively.
17569    ///
17570    /// Precedence: an explicitly written clause beats a statement-level flag.
17571    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17572    /// implicit `REPLACE` and `IGNORE` lowerings.
17573    fn parse_insert_conflict_clause(
17574        &mut self,
17575        replace: bool,
17576        ignore: bool,
17577    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17578        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17579            return Ok(Some(c));
17580        }
17581        if let Some(c) = self.parse_optional_on_conflict()? {
17582            return Ok(Some(c));
17583        }
17584        if replace {
17585            // REPLACE INTO = delete-then-insert, which PG spells as
17586            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17587            // reads an empty assignment list as "take the incoming row".
17588            return Ok(Some(crate::ast::OnConflictClause {
17589                target_columns: Vec::new(),
17590                index_where: None,
17591                constraint_name: None,
17592                mysql_lowered: true,
17593                action: crate::ast::OnConflictAction::Update {
17594                    assignments: Vec::new(),
17595                    where_: None,
17596                },
17597            }));
17598        }
17599        if ignore {
17600            return Ok(Some(Self::insert_ignore_clause()));
17601        }
17602        Ok(None)
17603    }
17604
17605    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17606    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17607    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17608    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17609    fn parse_optional_on_duplicate_key(
17610        &mut self,
17611    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17612        if !(matches!(self.peek(), Token::On)
17613            && matches!(self.tokens.get(self.pos + 1),
17614                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17615        {
17616            return Ok(None);
17617        }
17618        self.advance(); // ON
17619        self.advance(); // DUPLICATE
17620        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17621            return Err(self.err(format!(
17622                "expected KEY after ON DUPLICATE, got {:?}",
17623                self.peek()
17624            )));
17625        }
17626        self.advance();
17627        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17628            return Err(self.err(format!(
17629                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17630                self.peek()
17631            )));
17632        }
17633        self.advance();
17634        let mut assignments: Vec<(String, Expr)> = Vec::new();
17635        loop {
17636            let col = self.expect_ident_like()?;
17637            if !matches!(self.peek(), Token::Eq) {
17638                return Err(self.err(format!(
17639                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17640                    self.peek()
17641                )));
17642            }
17643            self.advance();
17644            let mut expr = self.parse_expr(0)?;
17645            Self::rewrite_mysql_values_refs(&mut expr);
17646            assignments.push((col, expr));
17647            if matches!(self.peek(), Token::Comma) {
17648                self.advance();
17649                continue;
17650            }
17651            break;
17652        }
17653        Ok(Some(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::Update {
17659                assignments,
17660                where_: None,
17661            },
17662        }))
17663    }
17664
17665    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17666        crate::ast::OnConflictClause {
17667            target_columns: Vec::new(),
17668            index_where: None,
17669            constraint_name: None,
17670            mysql_lowered: true,
17671            action: crate::ast::OnConflictAction::Nothing,
17672        }
17673    }
17674
17675    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17676        debug_assert!(
17677            matches!(self.peek(), Token::Insert)
17678                || (replace
17679                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17680        );
17681        self.advance();
17682        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17683        // would raise a duplicate-key error instead of failing the statement,
17684        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17685        // plain ident to the lexer; only the MySQL dialect accepts it here.
17686        let ignore = self.mysql_dialect
17687            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17688        if ignore {
17689            self.advance();
17690        }
17691        if !matches!(self.peek(), Token::Into) {
17692            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17693        }
17694        self.advance();
17695        let table = self.expect_ident_like()?;
17696        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17697        // grammar requires the AS keyword here (a bare identifier would be
17698        // ambiguous with a column list). The alias is what the ON CONFLICT
17699        // DO UPDATE expressions refer to the target row by.
17700        let alias = if matches!(self.peek(), Token::As) {
17701            self.advance();
17702            Some(self.expect_ident_like()?)
17703        } else {
17704            None
17705        };
17706        // v7.39 (round 428) — MySQL's SET-form INSERT:
17707        //     INSERT INTO t SET a = 1, b = 'x'
17708        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17709        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17710        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17711        // measured). So it lowers to the column list + one VALUES row and
17712        // rejoins the ordinary path, which already handles every one of
17713        // those. PG has no such spelling, hence the dialect gate.
17714        if self.mysql_dialect
17715            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17716        {
17717            self.advance(); // SET
17718            let mut names = Vec::new();
17719            let mut values = Vec::new();
17720            loop {
17721                names.push(self.expect_ident_like()?);
17722                if !matches!(self.peek(), Token::Eq) {
17723                    return Err(self.err(alloc::format!(
17724                        "expected '=' in INSERT … SET, got {:?}",
17725                        self.peek()
17726                    )));
17727                }
17728                self.advance();
17729                // `SET a = DEFAULT` rides the same `__column_default` marker
17730                // the VALUES-row and UPDATE-SET paths use; the INSERT
17731                // executor resolves it against the target column.
17732                if matches!(self.peek(), Token::Default) {
17733                    self.advance();
17734                    values.push(Expr::FunctionCall {
17735                        name: "__column_default".to_string(),
17736                        args: Vec::new(),
17737                    });
17738                } else {
17739                    values.push(self.parse_expr(0)?);
17740                }
17741                if matches!(self.peek(), Token::Comma) {
17742                    self.advance();
17743                    continue;
17744                }
17745                break;
17746            }
17747            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17748            let returning = self.parse_optional_returning()?;
17749            return Ok(Statement::Insert(InsertStatement {
17750                ctes: Vec::new(),
17751                table,
17752                alias,
17753                columns: Some(names),
17754                rows: alloc::vec![values],
17755                select_source: None,
17756                // MySQL's SET form has no `OVERRIDING …` clause (that is
17757                // PG's identity-column spelling).
17758                overriding: Overriding::None,
17759                mysql_ignore: ignore,
17760                on_conflict,
17761                returning,
17762            }));
17763        }
17764        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17765        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17766        // a parenthesized query source instead (PG select_with_parens:
17767        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17768        // both keywords are reserved in PG, so no column list can start
17769        // with them.
17770        let columns = if matches!(self.peek(), Token::LParen) {
17771            self.advance();
17772            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17773                let select_stmt = if self.peek_is_with_kw() {
17774                    self.advance();
17775                    self.parse_nested_with_select()?
17776                } else {
17777                    match self.parse_select_stmt()? {
17778                        Statement::Select(s) => s,
17779                        other => {
17780                            return Err(self.err(alloc::format!(
17781                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17782                            )));
17783                        }
17784                    }
17785                };
17786                if !matches!(self.peek(), Token::RParen) {
17787                    return Err(self.err(format!(
17788                        "expected ')' after parenthesized INSERT source, got {:?}",
17789                        self.peek()
17790                    )));
17791                }
17792                self.advance();
17793                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17794                let returning = self.parse_optional_returning()?;
17795                return Ok(Statement::Insert(InsertStatement {
17796                    ctes: Vec::new(),
17797                    table,
17798                    alias: alias.clone(),
17799                    columns: None,
17800                    rows: Vec::new(),
17801                    select_source: Some(Box::new(select_stmt)),
17802                    on_conflict,
17803                    returning,
17804                    overriding: Overriding::None,
17805                    mysql_ignore: ignore,
17806                }));
17807            }
17808            let mut names = Vec::new();
17809            loop {
17810                names.push(self.expect_ident_like()?);
17811                match self.peek() {
17812                    Token::Comma => {
17813                        self.advance();
17814                    }
17815                    Token::RParen => {
17816                        self.advance();
17817                        break;
17818                    }
17819                    other => {
17820                        return Err(self.err(format!(
17821                            "expected ',' or ')' in INSERT column list, got {other:?}"
17822                        )));
17823                    }
17824                }
17825            }
17826            Some(names)
17827        } else {
17828            None
17829        };
17830        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17831        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17832        // is captured on the statement so the engine can apply PG's
17833        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17834        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17835        {
17836            self.advance();
17837            let which = self.expect_ident_like()?;
17838            let ov = if which.eq_ignore_ascii_case("system") {
17839                Overriding::System
17840            } else if which.eq_ignore_ascii_case("user") {
17841                Overriding::User
17842            } else {
17843                return Err(self.err(format!(
17844                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17845                )));
17846            };
17847            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17848                return Err(self.err(format!(
17849                    "expected VALUE after OVERRIDING {}, got {:?}",
17850                    which.to_ascii_uppercase(),
17851                    self.peek()
17852                )));
17853            }
17854            self.advance();
17855            ov
17856        } else {
17857            Overriding::None
17858        };
17859        // `INSERT INTO t DEFAULT VALUES` — a single row made
17860        // entirely of column defaults. Lower to the permuted
17861        // column-list path with an empty list: every schema column
17862        // is unmapped, so the engine fills each from its default
17863        // (serials advance, plain defaults evaluate, the rest NULL).
17864        if matches!(self.peek(), Token::Default) {
17865            self.advance();
17866            if !matches!(self.peek(), Token::Values) {
17867                return Err(self.err(format!(
17868                    "expected VALUES after DEFAULT in INSERT, got {:?}",
17869                    self.peek()
17870                )));
17871            }
17872            self.advance();
17873            if columns.is_some() {
17874                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17875            }
17876            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17877            let returning = self.parse_optional_returning()?;
17878            return Ok(Statement::Insert(InsertStatement {
17879                ctes: Vec::new(),
17880                table,
17881                alias: alias.clone(),
17882                columns: Some(Vec::new()),
17883                rows: alloc::vec![Vec::new()],
17884                select_source: None,
17885                on_conflict,
17886                returning,
17887                overriding,
17888                mysql_ignore: ignore,
17889            }));
17890        }
17891        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17892        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17893        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17894        // SELECT …`) heads the SOURCE select, as in PG (the statement's
17895        // own WITH comes before INSERT).
17896        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17897            let select_stmt = if self.peek_is_with_kw() {
17898                self.advance();
17899                self.parse_nested_with_select()?
17900            } else {
17901                match self.parse_select_stmt()? {
17902                    Statement::Select(s) => s,
17903                    other => {
17904                        return Err(self.err(alloc::format!(
17905                            "expected SELECT after INSERT INTO ... target, got {other:?}"
17906                        )));
17907                    }
17908                }
17909            };
17910            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17911            let returning = self.parse_optional_returning()?;
17912            return Ok(Statement::Insert(InsertStatement {
17913                ctes: Vec::new(),
17914                table,
17915                alias: alias.clone(),
17916                columns,
17917                rows: Vec::new(),
17918                select_source: Some(Box::new(select_stmt)),
17919                on_conflict,
17920                returning,
17921                overriding,
17922                mysql_ignore: ignore,
17923            }));
17924        }
17925        if !matches!(self.peek(), Token::Values) {
17926            return Err(self.err(format!(
17927                "expected VALUES or SELECT after table name, got {:?}",
17928                self.peek()
17929            )));
17930        }
17931        self.advance();
17932        if !matches!(self.peek(), Token::LParen) {
17933            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17934        }
17935        let mut rows = Vec::new();
17936        loop {
17937            // Each iteration consumes one `(expr, expr, …)` tuple.
17938            if !matches!(self.peek(), Token::LParen) {
17939                return Err(self.err(format!(
17940                    "expected '(' for next VALUES tuple, got {:?}",
17941                    self.peek()
17942                )));
17943            }
17944            self.advance();
17945            let mut tuple = Vec::new();
17946            loop {
17947                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17948                // the column's declared default for that slot. Rides out as the
17949                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17950                // path uses; the INSERT executor resolves it per target column.
17951                if matches!(self.peek(), Token::Default) {
17952                    self.advance();
17953                    tuple.push(Expr::FunctionCall {
17954                        name: "__column_default".to_string(),
17955                        args: Vec::new(),
17956                    });
17957                } else {
17958                    tuple.push(self.parse_expr(0)?);
17959                }
17960                match self.peek() {
17961                    Token::Comma => {
17962                        self.advance();
17963                    }
17964                    Token::RParen => {
17965                        self.advance();
17966                        break;
17967                    }
17968                    other => {
17969                        return Err(self.err(format!(
17970                            "expected ',' or ')' in VALUES tuple, got {other:?}"
17971                        )));
17972                    }
17973                }
17974            }
17975            if tuple.is_empty() {
17976                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17977            }
17978            rows.push(tuple);
17979            // Continue with comma-separated tuples.
17980            if matches!(self.peek(), Token::Comma) {
17981                self.advance();
17982            } else {
17983                break;
17984            }
17985        }
17986        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17987        // to ON CONFLICT DO UPDATE with an empty conflict target
17988        // (the engine picks the table's first unique index, which
17989        // matches MySQL's any-unique-key behaviour for the common
17990        // single-key case). `VALUES(col)` in the assignments is
17991        // MySQL's spelling of EXCLUDED.col.
17992        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17993        let returning = self.parse_optional_returning()?;
17994        Ok(Statement::Insert(InsertStatement {
17995            ctes: Vec::new(),
17996            table,
17997            alias,
17998            columns,
17999            rows,
18000            select_source: None,
18001            on_conflict,
18002            returning,
18003            overriding,
18004            mysql_ignore: ignore,
18005        }))
18006    }
18007
18008    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18009    /// the incoming row's value — exactly PG's EXCLUDED.col.
18010    fn rewrite_mysql_values_refs(e: &mut Expr) {
18011        match e {
18012            Expr::FunctionCall { name, args }
18013                if name.eq_ignore_ascii_case("values")
18014                    && args.len() == 1
18015                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18016            {
18017                let Expr::Column(c) = &args[0] else {
18018                    unreachable!("guarded above");
18019                };
18020                *e = Expr::Column(crate::ast::ColumnName {
18021                    qualifier: Some("EXCLUDED".to_string()),
18022                    name: c.name.clone(),
18023                });
18024            }
18025            Expr::FunctionCall { args, .. } => {
18026                for a in args {
18027                    Self::rewrite_mysql_values_refs(a);
18028                }
18029            }
18030            Expr::Binary { lhs, rhs, .. } => {
18031                Self::rewrite_mysql_values_refs(lhs);
18032                Self::rewrite_mysql_values_refs(rhs);
18033            }
18034            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18035                Self::rewrite_mysql_values_refs(expr);
18036            }
18037            Expr::Case {
18038                operand,
18039                branches,
18040                else_branch,
18041            } => {
18042                if let Some(op) = operand {
18043                    Self::rewrite_mysql_values_refs(op);
18044                }
18045                for (w, t) in branches {
18046                    Self::rewrite_mysql_values_refs(w);
18047                    Self::rewrite_mysql_values_refs(t);
18048                }
18049                if let Some(el) = else_branch {
18050                    Self::rewrite_mysql_values_refs(el);
18051                }
18052            }
18053            _ => {}
18054        }
18055    }
18056
18057    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18058    /// clause sitting between the INSERT body and the trailing
18059    /// RETURNING. All keywords come in as bare idents; `ON` is
18060    /// a reserved Token though.
18061    fn parse_optional_on_conflict(
18062        &mut self,
18063    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18064        if !matches!(self.peek(), Token::On) {
18065            return Ok(None);
18066        }
18067        // Peek further: we want exactly "ON CONFLICT ...". If the
18068        // next ident isn't "conflict", let some other parser handle.
18069        let next_is_conflict = matches!(
18070            self.tokens.get(self.pos + 1),
18071            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18072        );
18073        if !next_is_conflict {
18074            return Ok(None);
18075        }
18076        self.advance(); // ON
18077        self.advance(); // CONFLICT
18078        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18079        // the constraint instead of listing columns (the pg_dump
18080        // form); the engine resolves it.
18081        let mut constraint_name: Option<String> = None;
18082        if matches!(self.peek(), Token::On) {
18083            self.advance(); // ON
18084            match self.advance() {
18085                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18086                }
18087                other => {
18088                    return Err(self.err(alloc::format!(
18089                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18090                    )));
18091                }
18092            }
18093            constraint_name = Some(self.expect_ident_like()?);
18094        }
18095        // Optional `(col [, col]*)` target list.
18096        let mut target_columns: Vec<String> = Vec::new();
18097        if matches!(self.peek(), Token::LParen) {
18098            self.advance();
18099            loop {
18100                target_columns.push(self.expect_ident_like()?);
18101                match self.peek() {
18102                    Token::Comma => {
18103                        self.advance();
18104                    }
18105                    Token::RParen => {
18106                        self.advance();
18107                        break;
18108                    }
18109                    other => {
18110                        return Err(self.err(alloc::format!(
18111                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18112                        )));
18113                    }
18114                }
18115            }
18116        }
18117        // v7.39 (round 240) — optional index predicate after the target
18118        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18119        // PARTIAL unique index; SPG's arbiters are full indexes, which
18120        // satisfy any predicate, so it is parsed and carried but not
18121        // consulted (recorded residual: partial-unique-index arbiters).
18122        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18123            self.advance();
18124            Some(self.parse_expr(0)?)
18125        } else {
18126            None
18127        };
18128        // Required `DO`.
18129        match self.advance() {
18130            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18131            other => {
18132                return Err(self.err(alloc::format!(
18133                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18134                )));
18135            }
18136        }
18137        // Action: NOTHING | UPDATE SET …
18138        let action = match self.advance() {
18139            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18140                crate::ast::OnConflictAction::Nothing
18141            }
18142            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18143                self.parse_on_conflict_update_action()?
18144            }
18145            other => {
18146                return Err(self.err(alloc::format!(
18147                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18148                )));
18149            }
18150        };
18151        Ok(Some(crate::ast::OnConflictClause {
18152            target_columns,
18153            index_where,
18154            constraint_name,
18155            mysql_lowered: false,
18156            action,
18157        }))
18158    }
18159
18160    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18161    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18162    /// consumed `UPDATE`.
18163    fn parse_on_conflict_update_action(
18164        &mut self,
18165    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18166        // `SET`
18167        match self.advance() {
18168            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18169            other => {
18170                return Err(self.err(alloc::format!(
18171                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18172                )));
18173            }
18174        }
18175        let mut assignments: Vec<(String, Expr)> = Vec::new();
18176        loop {
18177            let col = self.expect_ident_like()?;
18178            if !matches!(self.peek(), Token::Eq) {
18179                return Err(self.err(alloc::format!(
18180                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18181                    self.peek()
18182                )));
18183            }
18184            self.advance();
18185            let value = self.parse_expr(0)?;
18186            assignments.push((col, value));
18187            if matches!(self.peek(), Token::Comma) {
18188                self.advance();
18189                continue;
18190            }
18191            break;
18192        }
18193        let where_ = if matches!(self.peek(), Token::Where) {
18194            self.advance();
18195            Some(self.parse_expr(0)?)
18196        } else {
18197            None
18198        };
18199        Ok(crate::ast::OnConflictAction::Update {
18200            assignments,
18201            where_,
18202        })
18203    }
18204
18205    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18206        let mut items = Vec::new();
18207        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18208        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18209        // answers one zero-column row per row of t, and a bare `SELECT`
18210        // answers a single zero-column row. SPG required at least one
18211        // item, so both were syntax errors. Recognised by the token that
18212        // follows — nothing that can start an expression appears here.
18213        if self.select_list_is_empty_here() {
18214            return Ok(items);
18215        }
18216        loop {
18217            items.push(self.parse_select_item()?);
18218            if matches!(self.peek(), Token::Comma) {
18219                self.advance();
18220            } else {
18221                break;
18222            }
18223        }
18224        Ok(items)
18225    }
18226
18227    /// Is the target list empty at this point — i.e. does the next token
18228    /// end the SELECT's item list rather than start an item?
18229    fn select_list_is_empty_here(&self) -> bool {
18230        match self.peek() {
18231            Token::From
18232            | Token::Where
18233            | Token::Group
18234            | Token::Having
18235            | Token::Order
18236            | Token::Limit
18237            | Token::Offset
18238            | Token::Semicolon
18239            | Token::RParen
18240            | Token::Union
18241            | Token::Except
18242            | Token::Eof => true,
18243            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18244            // with unreserved keywords, so they arrive as plain idents.
18245            Token::Ident(s) => {
18246                s.eq_ignore_ascii_case("fetch")
18247                    || s.eq_ignore_ascii_case("window")
18248                    || s.eq_ignore_ascii_case("intersect")
18249            }
18250            _ => false,
18251        }
18252    }
18253
18254    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18255        if matches!(self.peek(), Token::Star) {
18256            self.advance();
18257            return Ok(SelectItem::Wildcard);
18258        }
18259        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18260        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18261        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18262        // `<ident> . *` with nothing binding tighter.
18263        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18264            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18265                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18266            {
18267                self.advance(); // qualifier
18268                self.advance(); // .
18269                self.advance(); // *
18270                return Ok(SelectItem::QualifiedWildcard(q));
18271            }
18272        }
18273        let start_tok = self.pos;
18274        let expr = self.parse_expr(0)?;
18275        let end_tok = self.consumed_pos();
18276        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18277        // multi-column function returns into columns. Marked here and lowered in
18278        // `parse_bare_select`, where the FROM clause is in hand.
18279        if matches!(self.peek(), Token::Dot)
18280            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18281        {
18282            self.advance(); // .
18283            self.advance(); // *
18284            return Ok(SelectItem::Expr {
18285                expr: Expr::FunctionCall {
18286                    name: "__record_expand".to_string(),
18287                    args: alloc::vec![expr],
18288                },
18289                alias: None,
18290            });
18291        }
18292        let alias = match self.parse_optional_alias()? {
18293            Some(a) => Some(a),
18294            None => self.mysql_item_label(&expr, start_tok, end_tok),
18295        };
18296        Ok(SelectItem::Expr { expr, alias })
18297    }
18298
18299    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18300    /// carries no `AS`, filled in here so every downstream path reports it
18301    /// without knowing the rule. `None` leaves the item un-aliased, which is
18302    /// what a PG session always gets.
18303    ///
18304    /// Measured against MariaDB 11, three rules and no more:
18305    ///
18306    /// | item             | label      | why                          |
18307    /// |------------------|------------|------------------------------|
18308    /// | `lbl.a`          | `a`        | a column reports its name    |
18309    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18310    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18311    ///
18312    /// The third is why this lives in the parser at all: the label is the
18313    /// text the client WROTE, down to the spacing, so it cannot be printed
18314    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18315    ///
18316    /// Comments survive, and that is right: through a `mariadb` CLI both
18317    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18318    /// CLIENT stripping the comment before it sends. Asked over the raw
18319    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18320    /// produces.
18321    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18322        if !self.mysql_dialect {
18323            return None;
18324        }
18325        match expr {
18326            // A column already reports its own name downstream; naming it
18327            // again here would only re-state the qualifier the label drops.
18328            Expr::Column(_) => None,
18329            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18330            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18331        }
18332    }
18333
18334    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18335    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18336    /// with PG's default column1..columnN names; subsequent rows
18337    /// chain as UNION ALL peers. Shared by the FROM-position
18338    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18339    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18340        let mut row_selects: Vec<SelectStatement> = Vec::new();
18341        loop {
18342            if !matches!(self.peek(), Token::LParen) {
18343                return Err(self.err(alloc::format!(
18344                    "expected '(' to start a VALUES row, got {:?}",
18345                    self.peek()
18346                )));
18347            }
18348            self.advance(); // (
18349            let mut items: Vec<SelectItem> = Vec::new();
18350            loop {
18351                let expr = self.parse_expr(0)?;
18352                items.push(SelectItem::Expr {
18353                    expr,
18354                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18355                });
18356                match self.peek() {
18357                    Token::Comma => {
18358                        self.advance();
18359                    }
18360                    Token::RParen => break,
18361                    other => {
18362                        return Err(self.err(alloc::format!(
18363                            "expected ',' or ')' in VALUES row, got {other:?}"
18364                        )));
18365                    }
18366                }
18367            }
18368            self.advance(); // )
18369            row_selects.push(SelectStatement {
18370                locking: None,
18371                ctes: Vec::new(),
18372                distinct: false,
18373                distinct_on: Vec::new(),
18374                items,
18375                from: None,
18376                where_: None,
18377                group_by: None,
18378                group_by_all: false,
18379                having: None,
18380                unions: Vec::new(),
18381                order_by: Vec::new(),
18382                limit: None,
18383                offset: None,
18384                limit_with_ties: false,
18385                window_check_exprs: Vec::new(),
18386            });
18387            if matches!(self.peek(), Token::Comma) {
18388                self.advance();
18389                continue;
18390            }
18391            break;
18392        }
18393        let mut head = row_selects.remove(0);
18394        head.unions = row_selects
18395            .into_iter()
18396            .map(|s| (UnionKind::All, s))
18397            .collect();
18398        Ok(head)
18399    }
18400
18401    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18402        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18403        // children. It was read as a table NAMED `only`, so the query
18404        // failed on `relation "only" does not exist`.
18405        //
18406        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18407        // absorbed the keyword, reasoning that SPG's children are
18408        // separate relations a plain scan does not descend into, so ONLY
18409        // already described the scan. That stopped being true when a
18410        // partition parent started unioning its children: measured,
18411        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18412        // where PG answers 0. The flag is carried now.
18413        let mut only = false;
18414        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18415            && matches!(
18416                self.tokens.get(self.pos + 1),
18417                Some(Token::Ident(_) | Token::QuotedIdent(_))
18418            )
18419        {
18420            only = true;
18421            self.advance();
18422        }
18423        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18424        // for these SRFs the keyword is noise at parse time: the
18425        // join executor already substitutes outer-column references
18426        // into unnest_expr / generate_series_args per outer row
18427        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18428        // licences the correlation even without the keyword. Absorb
18429        // it and fall through to the SRF arms below.
18430        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18431        // just the four builtin SRFs: a user set-returning function on a JOIN's
18432        // right side is the whole point of LATERAL. The keyword stays noise at
18433        // parse time — the join executor substitutes the outer row into the
18434        // call's arguments per outer row.
18435        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18436            && matches!(
18437                self.tokens.get(self.pos + 1),
18438                // The json_each family has its OWN `LATERAL …` arm below, which
18439                // needs to see the keyword — absorbing it here would send those
18440                // calls down the generic table-function channel instead.
18441                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18442            )
18443            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18444        {
18445            self.advance(); // LATERAL
18446        }
18447        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18448        // set-returning function whose argument may reference a
18449        // preceding FROM item. We rewrite this to
18450        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18451        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18452        // executor handles per-outer-row evaluation and the
18453        // SRF-primary jsonb_each_text path handles the inner
18454        // materialisation. Sentori 0067 backfill is the dogfood
18455        // shape.
18456        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18457            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18458            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18459        {
18460            self.advance(); // LATERAL
18461            let each_fn = match self.peek() {
18462                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18463                _ => unreachable!(),
18464            };
18465            self.advance(); // jsonb_each[_text] / json_each[_text]
18466            self.advance(); // (
18467            let arg = self.parse_expr(0)?;
18468            if !matches!(self.peek(), Token::RParen) {
18469                return Err(self.err(alloc::format!(
18470                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18471                    self.peek()
18472                )));
18473            }
18474            self.advance();
18475            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18476            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18477            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18478            //               FROM jsonb_each_text(<arg>) AS __srf__
18479            // PG's `AS kv(key, value)` column-alias list maps
18480            // positions to names; default to (key, value) when
18481            // omitted (matching the SRF's natural column names).
18482            let srf_alias = "__srf__".to_string();
18483            let key_alias = column_aliases
18484                .first()
18485                .cloned()
18486                .unwrap_or_else(|| "key".to_string());
18487            let value_alias = column_aliases
18488                .get(1)
18489                .cloned()
18490                .unwrap_or_else(|| "value".to_string());
18491            let inner_select = crate::ast::SelectStatement {
18492                locking: None,
18493                ctes: Vec::new(),
18494                distinct: false,
18495                distinct_on: Vec::new(),
18496                items: alloc::vec![
18497                    crate::ast::SelectItem::Expr {
18498                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18499                            qualifier: Some(srf_alias.clone()),
18500                            name: "key".to_string(),
18501                        }),
18502                        alias: Some(key_alias),
18503                    },
18504                    crate::ast::SelectItem::Expr {
18505                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18506                            qualifier: Some(srf_alias.clone()),
18507                            name: "value".to_string(),
18508                        }),
18509                        alias: Some(value_alias),
18510                    },
18511                ],
18512                from: Some(crate::ast::FromClause {
18513                    primary: TableRef {
18514                        name: srf_alias.clone(),
18515                        alias: Some(srf_alias.clone()),
18516                        only: false,
18517                        as_of_segment: None,
18518                        unnest_expr: None,
18519                        unnest_column_aliases: Vec::new(),
18520                        with_ordinality: false,
18521                        generate_series_args: None,
18522                        lateral_subquery: None,
18523                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18524                        table_fn_call: None,
18525                        rows_from: None,
18526                        json_table: None,
18527                        scalar_fn_item: false,
18528                    },
18529                    joins: Vec::new(),
18530                }),
18531                where_: None,
18532                group_by: None,
18533                group_by_all: false,
18534                having: None,
18535                unions: Vec::new(),
18536                order_by: Vec::new(),
18537                limit: None,
18538                offset: None,
18539                limit_with_ties: false,
18540                window_check_exprs: Vec::new(),
18541            };
18542            return Ok(TableRef {
18543                name: alias.clone(),
18544                alias: Some(alias),
18545                only: false,
18546                as_of_segment: None,
18547                unnest_expr: None,
18548                unnest_column_aliases: Vec::new(),
18549                with_ordinality: false,
18550                generate_series_args: None,
18551                lateral_subquery: Some(Box::new(inner_select)),
18552                jsonb_each_text_arg: None,
18553                table_fn_call: None,
18554                rows_from: None,
18555                json_table: None,
18556                scalar_fn_item: false,
18557            });
18558        }
18559        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18560        // without an explicit `LATERAL` keyword is the same shape
18561        // PG accepts (SRF naturally licences lateral correlation).
18562        // We mirror the LATERAL rewrite when the argument syntactic-
18563        // ally references an outer column (Column { qualifier:
18564        // Some(_), … }). For simplicity we apply the rewrite
18565        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18566        // in the FROM-list — caller-side join parsing positions
18567        // this peek correctly.
18568        // (Implementation note: detection lives below; the LATERAL
18569        // branch above already covers the explicit form; the bare
18570        // form falls through to the plain SRF arm and the engine
18571        // treats it as a constant-arg SRF if no outer reference is
18572        // present.)
18573        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18574        // table. Detect at the head so it claims precedence over
18575        // every other table-ref shape (unnest / generate_series /
18576        // bare ident); the lateral subquery itself follows the
18577        // regular SELECT grammar.
18578        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18579        // t(cols)`. Each row lowers to a constant SELECT with PG's
18580        // default column1..columnN names; subsequent rows chain as
18581        // UNION ALL peers. The result rides the derived-table
18582        // lateral_subquery channel — zero executor work.
18583        if matches!(self.peek(), Token::LParen)
18584            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18585        {
18586            self.advance(); // (
18587            self.advance(); // VALUES
18588            let head = self.parse_values_rows_body()?;
18589            if !matches!(self.peek(), Token::RParen) {
18590                return Err(self.err(alloc::format!(
18591                    "expected ')' after VALUES list, got {:?}",
18592                    self.peek()
18593                )));
18594            }
18595            self.advance();
18596            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18597            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18598            return Ok(TableRef {
18599                name,
18600                alias: alias_ident,
18601                only: false,
18602                as_of_segment: None,
18603                unnest_expr: None,
18604                unnest_column_aliases: column_aliases,
18605                with_ordinality: false,
18606                generate_series_args: None,
18607                lateral_subquery: Some(Box::new(head)),
18608                jsonb_each_text_arg: None,
18609                table_fn_call: None,
18610                rows_from: None,
18611                json_table: None,
18612                scalar_fn_item: false,
18613            });
18614        }
18615        // v7.37.17 (17.6 siblings) — plain derived table:
18616        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18617        // lateral_subquery channel the explicit LATERAL form uses —
18618        // an uncorrelated inner SELECT executes identically. The
18619        // inner parse carries UNION tails (they live on
18620        // SelectStatement.unions).
18621        // v7.37 D.20 — the derived-table inner may itself be a
18622        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18623        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18624        // bare `(SELECT …)`. parse_one_statement already routes a leading
18625        // `(` set-op group (its LParen arm) and a leading WITH
18626        // (parse_with_cte_then_select), so widen the second-token gate to
18627        // Select | LParen | WITH.
18628        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18629        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18630        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18631        // has existed since the shorthand landed and `parse_bare_select`
18632        // already routes it ("valid anywhere a SELECT head is"); what was
18633        // missing is this second-token gate, and the CTE body's dispatch
18634        // below. Round 868 found both by putting the shorthand in a
18635        // subquery — the top-level forms had been the only ones tested.
18636        if matches!(self.peek(), Token::LParen)
18637            && (matches!(
18638                self.tokens.get(self.pos + 1),
18639                Some(Token::Select | Token::LParen | Token::Table)
18640            ) || matches!(self.tokens.get(self.pos + 1),
18641                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18642        {
18643            self.advance(); // (
18644            let inner = match self.parse_one_statement()? {
18645                Statement::Select(s) => s,
18646                other => {
18647                    return Err(self.err(alloc::format!(
18648                        "expected SELECT inside derived table ( … ), got {other:?}"
18649                    )));
18650                }
18651            };
18652            if !matches!(self.peek(), Token::RParen) {
18653                return Err(self.err(alloc::format!(
18654                    "expected ')' after derived-table subquery, got {:?}",
18655                    self.peek()
18656                )));
18657            }
18658            self.advance();
18659            // `AS t(a, b)` column-alias list rides the
18660            // unnest_column_aliases field (same positional-rename
18661            // contract the unnest SRFs use).
18662            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18663            let name = alias_ident
18664                .clone()
18665                .unwrap_or_else(|| "subquery".to_string());
18666            return Ok(TableRef {
18667                name,
18668                alias: alias_ident,
18669                only: false,
18670                as_of_segment: None,
18671                unnest_expr: None,
18672                unnest_column_aliases: column_aliases,
18673                with_ordinality: false,
18674                generate_series_args: None,
18675                lateral_subquery: Some(Box::new(inner)),
18676                jsonb_each_text_arg: None,
18677                table_fn_call: None,
18678                rows_from: None,
18679                json_table: None,
18680                scalar_fn_item: false,
18681            });
18682        }
18683        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18684            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18685        {
18686            self.advance(); // LATERAL
18687            self.advance(); // (
18688            // Parse the inner SELECT.
18689            let inner = match self.parse_one_statement()? {
18690                Statement::Select(s) => s,
18691                other => {
18692                    return Err(self.err(alloc::format!(
18693                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18694                    )));
18695                }
18696            };
18697            if !matches!(self.peek(), Token::RParen) {
18698                return Err(self.err(alloc::format!(
18699                    "expected ')' after LATERAL subquery, got {:?}",
18700                    self.peek()
18701                )));
18702            }
18703            self.advance();
18704            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18705            // `(VALUES …) t(g)` derived table round-trips through view-body
18706            // Display, which renders on the lateral_subquery channel).
18707            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18708            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18709            return Ok(TableRef {
18710                name,
18711                alias: alias_ident,
18712                only: false,
18713                as_of_segment: None,
18714                unnest_expr: None,
18715                unnest_column_aliases: column_aliases,
18716                with_ordinality: false,
18717                generate_series_args: None,
18718                lateral_subquery: Some(Box::new(inner)),
18719                jsonb_each_text_arg: None,
18720                table_fn_call: None,
18721                rows_from: None,
18722                json_table: None,
18723                scalar_fn_item: false,
18724            });
18725        }
18726        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18727        // function as a FROM item. Emits one row per (key, value)
18728        // pair in the JSONB object argument as TEXT columns. May
18729        // be wrapped in CROSS JOIN LATERAL when the argument
18730        // references a preceding FROM item (sentori migration
18731        // 0067 backfill shape: `CROSS JOIN LATERAL
18732        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18733        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18734            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18735        {
18736            let each_fn = match self.peek() {
18737                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18738                _ => unreachable!(),
18739            };
18740            self.advance(); // jsonb_each[_text] / json_each[_text]
18741            self.advance(); // (
18742            let arg = self.parse_expr(0)?;
18743            if !matches!(self.peek(), Token::RParen) {
18744                return Err(self.err(alloc::format!(
18745                    "expected ')' after {each_fn}() argument, got {:?}",
18746                    self.peek()
18747                )));
18748            }
18749            self.advance();
18750            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18751            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18752            return Ok(TableRef {
18753                name,
18754                alias: alias_ident,
18755                only: false,
18756                as_of_segment: None,
18757                unnest_expr: None,
18758                // `AS t(k, v)` renames key/value positionally, same as the
18759                // LATERAL-position form already does.
18760                unnest_column_aliases: column_aliases,
18761                with_ordinality: false,
18762                generate_series_args: None,
18763                lateral_subquery: None,
18764                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18765                table_fn_call: None,
18766                rows_from: None,
18767                json_table: None,
18768                scalar_fn_item: false,
18769            });
18770        }
18771        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18772        // (+ json_ variants) — record-returning JSON functions with a
18773        // column-definition list. Desugar to a derived table that
18774        // projects each declared column from the JSON via `->>` + a cast,
18775        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18776        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18777            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18778        {
18779            return self.parse_json_to_record_from();
18780        }
18781        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18782        // row is a text[] of capture groups, so it cannot desugar to unnest
18783        // (that would flatten the array). Wrap it as a derived table
18784        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18785        // SRF path already emits one text[] row per match. PG names the column
18786        // `regexp_matches`; an `AS a(col)` alias overrides it.
18787        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18788                if s.eq_ignore_ascii_case("regexp_matches"))
18789            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18790        {
18791            self.advance(); // fn name
18792            self.advance(); // (
18793            let mut fn_args: Vec<Expr> = Vec::new();
18794            loop {
18795                fn_args.push(self.parse_expr(0)?);
18796                if matches!(self.peek(), Token::Comma) {
18797                    self.advance();
18798                    continue;
18799                }
18800                break;
18801            }
18802            if !matches!(self.peek(), Token::RParen) {
18803                return Err(self.err(alloc::format!(
18804                    "expected ')' after regexp_matches() arguments, got {:?}",
18805                    self.peek()
18806                )));
18807            }
18808            self.advance();
18809            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18810            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18811            // it, so it died on the `with` token while every other table function
18812            // accepted it.
18813            let with_ordinality = self.absorb_with_ordinality();
18814            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18815            let table_alias = alias_ident
18816                .clone()
18817                .unwrap_or_else(|| "regexp_matches".to_string());
18818            // PG names a single-column function's output column after the ALIAS
18819            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18820            // `m` reads as that column and not as a whole-row composite. Naming
18821            // it after the function regardless made `SELECT m[1] FROM … AS m`
18822            // subscript a record.
18823            let col_name = column_aliases
18824                .first()
18825                .cloned()
18826                .or_else(|| alias_ident.clone())
18827                .unwrap_or_else(|| "regexp_matches".to_string());
18828            let inner = crate::ast::SelectStatement {
18829                locking: None,
18830                ctes: Vec::new(),
18831                distinct: false,
18832                distinct_on: Vec::new(),
18833                items: alloc::vec![SelectItem::Expr {
18834                    expr: Expr::FunctionCall {
18835                        name: "regexp_matches".to_string(),
18836                        args: fn_args,
18837                    },
18838                    alias: Some(col_name),
18839                }],
18840                from: None,
18841                where_: None,
18842                group_by: None,
18843                group_by_all: false,
18844                having: None,
18845                unions: Vec::new(),
18846                order_by: Vec::new(),
18847                limit: None,
18848                offset: None,
18849                limit_with_ties: false,
18850                window_check_exprs: Vec::new(),
18851            };
18852            return Ok(TableRef {
18853                name: table_alias.clone(),
18854                alias: Some(table_alias),
18855                only: false,
18856                as_of_segment: None,
18857                unnest_expr: None,
18858                unnest_column_aliases: column_aliases,
18859                with_ordinality,
18860                generate_series_args: None,
18861                lateral_subquery: Some(Box::new(inner)),
18862                jsonb_each_text_arg: None,
18863                table_fn_call: None,
18864                rows_from: None,
18865                json_table: None,
18866                // regexp_matches returns text[], a base type: `SELECT m FROM
18867                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18868                scalar_fn_item: true,
18869            });
18870        }
18871        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18872        // / json_ variants as a FROM item. Rewritten into
18873        // `unnest(<same fn>(<expr>))`: the scalar form returns the
18874        // elements as a TEXT array, and the existing unnest SRF path
18875        // materialises one row per element. PG's natural column name
18876        // is `value`; an `AS a(col)` column-alias list overrides it.
18877        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18878                if s.eq_ignore_ascii_case("jsonb_array_elements")
18879                    || s.eq_ignore_ascii_case("json_array_elements")
18880                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18881                    || s.eq_ignore_ascii_case("json_array_elements_text")
18882                    || s.eq_ignore_ascii_case("jsonb_object_keys")
18883                    || s.eq_ignore_ascii_case("json_object_keys")
18884                    || s.eq_ignore_ascii_case("jsonb_path_query")
18885                    || s.eq_ignore_ascii_case("json_path_query")
18886                    || s.eq_ignore_ascii_case("generate_subscripts")
18887                    || s.eq_ignore_ascii_case("string_to_table")
18888                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
18889            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18890        {
18891            let fn_name = match self.peek() {
18892                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18893                _ => unreachable!(),
18894            };
18895            self.advance(); // fn name
18896            self.advance(); // (
18897            let mut fn_args: Vec<Expr> = Vec::new();
18898            loop {
18899                fn_args.push(self.parse_expr(0)?);
18900                if matches!(self.peek(), Token::Comma) {
18901                    self.advance();
18902                    continue;
18903                }
18904                break;
18905            }
18906            if !matches!(self.peek(), Token::RParen) {
18907                return Err(self.err(alloc::format!(
18908                    "expected ')' after {fn_name}() arguments, got {:?}",
18909                    self.peek()
18910                )));
18911            }
18912            self.advance();
18913            let with_ordinality = self.absorb_with_ordinality();
18914            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18915            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18916            // PG's natural column name: the array-elements SRFs
18917            // declare an OUT parameter `value`; jsonb_object_keys
18918            // and generate_subscripts have none, so the column is
18919            // named after the function. A bare table alias on a
18920            // single-column SRF renames the column too (PG: `FROM
18921            // generate_subscripts(a, 1) AS s` projects column s) —
18922            // except for the OUT-parameter SRFs, whose column stays
18923            // `value` under a bare alias.
18924            let natural_col = if fn_name.ends_with("_array_elements")
18925                || fn_name.ends_with("_array_elements_text")
18926            {
18927                "value".to_string()
18928            } else {
18929                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18930            };
18931            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18932            // Keep any further entries — the second names the
18933            // ordinality column under WITH ORDINALITY.
18934            srf_cols.extend(column_aliases.into_iter().skip(1));
18935            // The *_to_table SRFs are row-streams over the existing
18936            // *_to_array scalars — map the call target; the display
18937            // name (alias / column defaults) keeps the SRF spelling.
18938            let call_name = match fn_name.as_str() {
18939                "string_to_table" => "string_to_array".to_string(),
18940                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18941                _ => fn_name,
18942            };
18943            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18944            // preceding FROM item (bare or qualified column) is correlated;
18945            // route it through the per-outer-row lateral channel.
18946            let expr = crate::ast::Expr::FunctionCall {
18947                name: call_name,
18948                args: fn_args,
18949            };
18950            let correlated = Self::expr_has_any_column(&expr);
18951            let tref = TableRef {
18952                name,
18953                alias: alias_ident,
18954                only: false,
18955                as_of_segment: None,
18956                unnest_expr: Some(Box::new(expr)),
18957                unnest_column_aliases: srf_cols,
18958                with_ordinality,
18959                generate_series_args: None,
18960                lateral_subquery: None,
18961                jsonb_each_text_arg: None,
18962                table_fn_call: None,
18963                rows_from: None,
18964                json_table: None,
18965                // Each of these returns a BASE type (jsonb / text / int), so the item's
18966                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18967                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18968                scalar_fn_item: !with_ordinality,
18969            };
18970            return Ok(if correlated {
18971                Self::wrap_correlated_srf(tref)
18972            } else {
18973                tref
18974            });
18975        }
18976        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18977        // explicit parallel-zip syntax. Each entry lowers to its
18978        // array-returning scalar form (unnest(x) → x itself; the
18979        // FROM-SRF rewrite family → their scalar array calls) and
18980        // the list rides the multi-arg unnest zip channel:
18981        // NULL-padded to the longest, WITH ORDINALITY appends the
18982        // counter. generate_series has no scalar array form and
18983        // errors honestly.
18984        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18985            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18986            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18987        {
18988            self.advance(); // ROWS
18989            self.advance(); // FROM
18990            self.advance(); // (
18991            let mut entries: Vec<Expr> = Vec::new();
18992            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18993            // Used only when some entry has no array form.
18994            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18995            loop {
18996                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18997                if !matches!(self.peek(), Token::LParen) {
18998                    return Err(self.err(alloc::format!(
18999                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19000                        self.peek()
19001                    )));
19002                }
19003                self.advance();
19004                let mut fn_args: Vec<Expr> = Vec::new();
19005                if !matches!(self.peek(), Token::RParen) {
19006                    loop {
19007                        fn_args.push(self.parse_expr(0)?);
19008                        if matches!(self.peek(), Token::Comma) {
19009                            self.advance();
19010                            continue;
19011                        }
19012                        break;
19013                    }
19014                }
19015                if !matches!(self.peek(), Token::RParen) {
19016                    return Err(self.err(alloc::format!(
19017                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19018                        self.peek()
19019                    )));
19020                }
19021                self.advance();
19022                let entry = match fn_name.as_str() {
19023                    "unnest" => {
19024                        if fn_args.len() != 1 {
19025                            return Err(
19026                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19027                            );
19028                        }
19029                        fn_args.pop().expect("len checked")
19030                    }
19031                    "jsonb_array_elements"
19032                    | "json_array_elements"
19033                    | "jsonb_array_elements_text"
19034                    | "json_array_elements_text"
19035                    | "jsonb_object_keys"
19036                    | "json_object_keys"
19037                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19038                        name: fn_name,
19039                        args: fn_args,
19040                    },
19041                    "string_to_table" => crate::ast::Expr::FunctionCall {
19042                        name: "string_to_array".to_string(),
19043                        args: fn_args,
19044                    },
19045                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19046                        name: "regexp_split_to_array".to_string(),
19047                        args: fn_args,
19048                    },
19049                    // v7.39 (read01 round 74) — an SRF with no array form
19050                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19051                    // scalar expression to zip, so the WHOLE list switches to the
19052                    // rows_from channel, which runs each function and zips the
19053                    // rows themselves. The all-array case keeps the old lowering:
19054                    // it is well-trodden and this must not disturb it.
19055                    _ => {
19056                        generic.push((fn_name, fn_args));
19057                        if matches!(self.peek(), Token::Comma) {
19058                            self.advance();
19059                            continue;
19060                        }
19061                        break;
19062                    }
19063                };
19064                generic.push((
19065                    // The array-able entries carry their lowered expr along, so a
19066                    // MIXED list still works: the engine sees the scalar array
19067                    // form and unnests it.
19068                    "__array".to_string(),
19069                    alloc::vec![entry.clone()],
19070                ));
19071                entries.push(entry);
19072                if matches!(self.peek(), Token::Comma) {
19073                    self.advance();
19074                    continue;
19075                }
19076                break;
19077            }
19078            if !matches!(self.peek(), Token::RParen) {
19079                return Err(self.err(alloc::format!(
19080                    "expected ')' to close ROWS FROM, got {:?}",
19081                    self.peek()
19082                )));
19083            }
19084            self.advance();
19085            let with_ordinality = self.absorb_with_ordinality();
19086            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19087            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19088            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19089            // list rides the generic channel.
19090            if generic.iter().any(|(n, _)| n != "__array") {
19091                let correlated = generic
19092                    .iter()
19093                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19094                let tref = TableRef {
19095                    name,
19096                    alias: alias_ident,
19097                    only: false,
19098                    as_of_segment: None,
19099                    unnest_expr: None,
19100                    unnest_column_aliases,
19101                    with_ordinality,
19102                    generate_series_args: None,
19103                    lateral_subquery: None,
19104                    jsonb_each_text_arg: None,
19105                    table_fn_call: None,
19106                    rows_from: Some(generic),
19107                    json_table: None,
19108                    scalar_fn_item: false,
19109                };
19110                return Ok(if correlated {
19111                    Self::wrap_correlated_srf(tref)
19112                } else {
19113                    tref
19114                });
19115            }
19116            let correlated = entries.iter().any(Self::expr_has_any_column);
19117            let expr = if entries.len() == 1 {
19118                entries.pop().expect("len checked")
19119            } else {
19120                crate::ast::Expr::FunctionCall {
19121                    name: "__unnest_zip".to_string(),
19122                    args: entries,
19123                }
19124            };
19125            let tref = TableRef {
19126                name,
19127                alias: alias_ident,
19128                only: false,
19129                as_of_segment: None,
19130                unnest_expr: Some(Box::new(expr)),
19131                unnest_column_aliases,
19132                with_ordinality,
19133                generate_series_args: None,
19134                lateral_subquery: None,
19135                jsonb_each_text_arg: None,
19136                table_fn_call: None,
19137                rows_from: None,
19138                json_table: None,
19139                scalar_fn_item: false,
19140            };
19141            return Ok(if correlated {
19142                Self::wrap_correlated_srf(tref)
19143            } else {
19144                tref
19145            });
19146        }
19147        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19148        // source. Detect at the head before the bare-ident fallback;
19149        // unnest is not a reserved token.
19150        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19151            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19152        {
19153            self.advance(); // unnest
19154            self.advance(); // (
19155            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19156            while matches!(self.peek(), Token::Comma) {
19157                self.advance();
19158                srf_args.push(self.parse_expr(0)?);
19159            }
19160            if !matches!(self.peek(), Token::RParen) {
19161                return Err(self.err(alloc::format!(
19162                    "expected ')' after unnest() argument, got {:?}",
19163                    self.peek()
19164                )));
19165            }
19166            self.advance();
19167            // Multi-arg unnest(a, b, …) zips the arrays in
19168            // parallel, NULL-padding to the longest (PG's ROWS
19169            // FROM shorthand). Lower onto the unnest channel as an
19170            // internal marker call the executors unpack.
19171            let expr = if srf_args.len() == 1 {
19172                srf_args.pop().expect("len checked")
19173            } else {
19174                crate::ast::Expr::FunctionCall {
19175                    name: "__unnest_zip".to_string(),
19176                    args: srf_args,
19177                }
19178            };
19179            let with_ordinality = self.absorb_with_ordinality();
19180            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19181            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19182            let correlated = Self::expr_has_any_column(&expr);
19183            let tref = TableRef {
19184                name,
19185                alias: alias_ident,
19186                only: false,
19187                as_of_segment: None,
19188                unnest_expr: Some(Box::new(expr)),
19189                unnest_column_aliases,
19190                with_ordinality,
19191                generate_series_args: None,
19192                lateral_subquery: None,
19193                jsonb_each_text_arg: None,
19194                table_fn_call: None,
19195                rows_from: None,
19196                json_table: None,
19197                scalar_fn_item: false,
19198            };
19199            return Ok(if correlated {
19200                Self::wrap_correlated_srf(tref)
19201            } else {
19202                tref
19203            });
19204        }
19205        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19206        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19207        // generic table-fn arg parser can't read), so it is intercepted
19208        // here BEFORE the generic dispatch. The doc expr may reference
19209        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19210        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19211                if s.eq_ignore_ascii_case("json_table"))
19212            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19213        {
19214            let tref = self.parse_json_table_ref()?;
19215            let correlated = tref
19216                .json_table
19217                .as_deref()
19218                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19219            return Ok(if correlated {
19220                Self::wrap_correlated_srf(tref)
19221            } else {
19222                tref
19223            });
19224        }
19225        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19226        // functions dispatched by name (`pg_partition_tree('t')`,
19227        // `pg_partition_ancestors('t')`). Same head-detection shape as
19228        // unnest; the engine executor owns the row shape per function.
19229        // v7.39 (read01 round 65) — and a USER function in FROM position
19230        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19231        // (generate_series / unnest / the json_each family) keep it — their arms
19232        // sit further down, so they are excluded here by name rather than by
19233        // ordering. Anything else that is an ident followed by `(` is a table
19234        // function; the engine executor decides whether it is a builtin, a
19235        // set-returning user function, or an error.
19236        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19237                if !s.eq_ignore_ascii_case("generate_series")
19238                    && !s.eq_ignore_ascii_case("unnest")
19239                    && !is_json_each_name(s))
19240            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19241        {
19242            // Body out-of-line — this parse sits on the FROM/subquery
19243            // recursion chain (debug frame-cliff discipline).
19244            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19245            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19246            // outer row, so it rides the lateral channel. Same rule the unnest
19247            // arm uses.
19248            let tref = self.parse_table_fn_ref()?;
19249            let correlated = tref
19250                .table_fn_call
19251                .as_deref()
19252                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19253            return Ok(if correlated {
19254                Self::wrap_correlated_srf(tref)
19255            } else {
19256                tref
19257            });
19258        }
19259        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19260        // [, step])` set-returning source. Same shape as unnest:
19261        // detect at the head, parse the comma-separated arg list,
19262        // dispatch downstream through the engine's set-returning
19263        // path. Supports integer triplets (mailrs's `WITH row_no AS
19264        // (SELECT * FROM generate_series(1, N))` pattern) and
19265        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19266        // date-range iteration pattern, which pre-3.10 had no
19267        // direct equivalent in SPG).
19268        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19269            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19270        {
19271            self.advance(); // generate_series
19272            self.advance(); // (
19273            let mut args: Vec<Expr> = Vec::new();
19274            loop {
19275                args.push(self.parse_expr(0)?);
19276                if matches!(self.peek(), Token::Comma) {
19277                    self.advance();
19278                    continue;
19279                }
19280                break;
19281            }
19282            if !matches!(self.peek(), Token::RParen) {
19283                return Err(self.err(alloc::format!(
19284                    "expected ')' after generate_series() arguments, got {:?}",
19285                    self.peek()
19286                )));
19287            }
19288            self.advance();
19289            if args.len() < 2 || args.len() > 3 {
19290                return Err(self.err(alloc::format!(
19291                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19292                    args.len()
19293                )));
19294            }
19295            let with_ordinality = self.absorb_with_ordinality();
19296            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19297            let name = alias_ident
19298                .clone()
19299                .unwrap_or_else(|| "generate_series".to_string());
19300            let correlated = args.iter().any(Self::expr_has_any_column);
19301            let tref = TableRef {
19302                name,
19303                alias: alias_ident,
19304                only: false,
19305                as_of_segment: None,
19306                unnest_expr: None,
19307                unnest_column_aliases: column_aliases,
19308                with_ordinality,
19309                generate_series_args: Some(args),
19310                lateral_subquery: None,
19311                jsonb_each_text_arg: None,
19312                table_fn_call: None,
19313                rows_from: None,
19314                json_table: None,
19315                scalar_fn_item: false,
19316            };
19317            return Ok(if correlated {
19318                Self::wrap_correlated_srf(tref)
19319            } else {
19320                tref
19321            });
19322        }
19323        // v7.16.2 — preserve information_schema / pg_catalog
19324        // qualifiers (mailrs round-10 A.3). The generic
19325        // `expect_ident_like` strip silently drops the schema;
19326        // we want the engine to recognise these PG meta tables
19327        // and synthesise rows from the live catalog. Produce a
19328        // synthetic name (`__spg_info_columns` etc.) so the
19329        // engine's SELECT-side router can dispatch without
19330        // clashing with any user-defined `columns` table.
19331        let name = if let Some(synth) = self.try_peek_meta_qualified() {
19332            synth
19333        } else if let Some(synth) = self.try_peek_meta_bare() {
19334            synth
19335        } else {
19336            self.expect_ident_like()?
19337        };
19338        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19339        // time-travel clause. Parse BEFORE the alias so the
19340        // alias can still ride at the tail (`tbl AS OF SEGMENT
19341        // '5' alias`). `AS` is a reserved keyword token, while
19342        // `OF` and `SEGMENT` are bare idents.
19343        let as_of_segment = if matches!(self.peek(), Token::As)
19344            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19345        {
19346            self.advance(); // AS
19347            self.advance(); // OF
19348            let kw = match self.peek().clone() {
19349                Token::Ident(s) | Token::QuotedIdent(s) => s,
19350                other => {
19351                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19352                }
19353            };
19354            if !kw.eq_ignore_ascii_case("segment") {
19355                return Err(self.err(format!(
19356                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19357                )));
19358            }
19359            self.advance();
19360            // Segment id literal — accept either a string or
19361            // integer for operator ergonomics.
19362            let id = match self.advance() {
19363                Token::String(s) => s
19364                    .parse::<u32>()
19365                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19366                Token::Integer(n) => u32::try_from(n)
19367                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19368                other => {
19369                    return Err(self.err(format!(
19370                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19371                    )));
19372                }
19373            };
19374            Some(id)
19375        } else {
19376            None
19377        };
19378        // TABLESAMPLE is not a reserved token — keep the bare-ident
19379        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19380        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19381        {
19382            None
19383        } else {
19384            self.parse_optional_alias()?
19385        };
19386        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19387        // (PG grammar). BERNOULLI lowers to a per-row
19388        // `random() < p/100` conjunct on the enclosing SELECT's
19389        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19390        // shares the lowering: SPG has no page structure to
19391        // sample, and the row-level form returns the same expected
19392        // fraction. REPEATABLE(seed) promises a deterministic
19393        // sample SPG cannot honour yet — honest error rather than
19394        // a silently ignored seed.
19395        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19396            self.advance();
19397            let method = self.expect_ident_like()?;
19398            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19399                return Err(self.err(alloc::format!(
19400                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19401                )));
19402            }
19403            if !matches!(self.peek(), Token::LParen) {
19404                return Err(self.err(alloc::format!(
19405                    "expected '(' after TABLESAMPLE {}, got {:?}",
19406                    method.to_ascii_uppercase(),
19407                    self.peek()
19408                )));
19409            }
19410            self.advance();
19411            let percent = self.parse_expr(0)?;
19412            if !matches!(self.peek(), Token::RParen) {
19413                return Err(self.err(alloc::format!(
19414                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19415                    self.peek()
19416                )));
19417            }
19418            self.advance();
19419            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19420            // `seed`, so the sample is stable across repeats and rescans.
19421            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19422            let mut sample_seed: Option<Expr> = None;
19423            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19424                self.advance();
19425                if !matches!(self.peek(), Token::LParen) {
19426                    return Err(self.err(alloc::format!(
19427                        "expected '(' after REPEATABLE, got {:?}",
19428                        self.peek()
19429                    )));
19430                }
19431                self.advance();
19432                let seed = self.parse_expr(0)?;
19433                if !matches!(self.peek(), Token::RParen) {
19434                    return Err(self.err(alloc::format!(
19435                        "expected ')' after REPEATABLE seed, got {:?}",
19436                        self.peek()
19437                    )));
19438                }
19439                self.advance();
19440                sample_seed = Some(seed);
19441            }
19442            let draw = match sample_seed {
19443                Some(seed) => Expr::FunctionCall {
19444                    name: "__tsm_fract".to_string(),
19445                    args: alloc::vec![seed],
19446                },
19447                None => Expr::FunctionCall {
19448                    name: "random".to_string(),
19449                    args: Vec::new(),
19450                },
19451            };
19452            self.pending_sample_preds.push(Expr::Binary {
19453                lhs: Box::new(draw),
19454                op: crate::ast::BinOp::Lt,
19455                rhs: Box::new(Expr::Binary {
19456                    lhs: Box::new(percent),
19457                    op: crate::ast::BinOp::Div,
19458                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19459                }),
19460            });
19461        }
19462        Ok(TableRef {
19463            name,
19464            alias,
19465            only,
19466            as_of_segment,
19467            unnest_expr: None,
19468            unnest_column_aliases: Vec::new(),
19469            with_ordinality: false,
19470            generate_series_args: None,
19471            lateral_subquery: None,
19472            jsonb_each_text_arg: None,
19473            table_fn_call: None,
19474            rows_from: None,
19475            json_table: None,
19476            scalar_fn_item: false,
19477        })
19478    }
19479
19480    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19481    /// but also accepts `AS alias(col [, col, …])` — the
19482    /// PG-standard table-function column-list form. The column
19483    /// list is only honoured when paired with `UNNEST(...)` in
19484    /// the parent; other call sites currently discard it.
19485    /// True when the expression tree contains a qualified column
19486    /// reference (`t.col`) — the syntactic marker that an SRF
19487    /// argument correlates with a preceding FROM item.
19488    fn expr_has_qualified_column(e: &Expr) -> bool {
19489        match e {
19490            Expr::Column(c) => c.qualifier.is_some(),
19491            Expr::Binary { lhs, rhs, .. } => {
19492                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19493            }
19494            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19495            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19496            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19497            Expr::Case {
19498                operand,
19499                branches,
19500                else_branch,
19501            } => {
19502                operand
19503                    .as_deref()
19504                    .is_some_and(Self::expr_has_qualified_column)
19505                    || branches.iter().any(|(w, t)| {
19506                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19507                    })
19508                    || else_branch
19509                        .as_deref()
19510                        .is_some_and(Self::expr_has_qualified_column)
19511            }
19512            _ => false,
19513        }
19514    }
19515
19516    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19517    /// counts a bare (unqualified) column. A set-returning function has no
19518    /// input columns of its own, so ANY column in its arguments is an outer
19519    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19520    fn expr_has_any_column(e: &Expr) -> bool {
19521        match e {
19522            Expr::Column(_) => true,
19523            Expr::Binary { lhs, rhs, .. } => {
19524                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19525            }
19526            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19527            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19528            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19529            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19530            // constructor or subscript fell to the `_ => false` arm, so
19531            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19532            // channel and the eager peer eval answered `column "x" does
19533            // not exist` (the substitution walker already recurses both
19534            // shapes; only this detector was blind to them).
19535            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19536            Expr::ArraySubscript { target, index } => {
19537                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19538            }
19539            Expr::Case {
19540                operand,
19541                branches,
19542                else_branch,
19543            } => {
19544                operand.as_deref().is_some_and(Self::expr_has_any_column)
19545                    || branches
19546                        .iter()
19547                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19548                    || else_branch
19549                        .as_deref()
19550                        .is_some_and(Self::expr_has_any_column)
19551            }
19552            _ => false,
19553        }
19554    }
19555
19556    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19557    /// `generate_series(1, t.n)`) into the lateral_subquery
19558    /// channel: `SELECT * FROM <srf>` executes per outer row with
19559    /// outer references substituted (v7.37.43-T4.5 machinery).
19560    /// Uncorrelated SRFs stay on their plain channels.
19561    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19562        let name = srf.name.clone();
19563        let alias = srf.alias.clone();
19564        let inner = crate::ast::SelectStatement {
19565            locking: None,
19566            ctes: Vec::new(),
19567            distinct: false,
19568            distinct_on: Vec::new(),
19569            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19570            from: Some(crate::ast::FromClause {
19571                primary: srf,
19572                joins: Vec::new(),
19573            }),
19574            where_: None,
19575            group_by: None,
19576            group_by_all: false,
19577            having: None,
19578            unions: Vec::new(),
19579            order_by: Vec::new(),
19580            limit: None,
19581            offset: None,
19582            limit_with_ties: false,
19583            window_check_exprs: Vec::new(),
19584        };
19585        TableRef {
19586            name,
19587            alias,
19588            only: false,
19589            as_of_segment: None,
19590            unnest_expr: None,
19591            unnest_column_aliases: Vec::new(),
19592            with_ordinality: false,
19593            generate_series_args: None,
19594            lateral_subquery: Some(Box::new(inner)),
19595            jsonb_each_text_arg: None,
19596            table_fn_call: None,
19597            rows_from: None,
19598            json_table: None,
19599            scalar_fn_item: false,
19600        }
19601    }
19602
19603    /// True when the expression tree contains an unresolved
19604    /// `OVER w` marker (see parse_over_clause).
19605    fn expr_has_named_window(e: &Expr) -> bool {
19606        match e {
19607            Expr::WindowFunction { partition_by, .. } => matches!(
19608                partition_by.as_slice(),
19609                [Expr::Column(c)] if matches!(
19610                    c.qualifier.as_deref(),
19611                    Some("__named_window__") | Some("__named_window_ref__")
19612                )
19613            ),
19614            Expr::Binary { lhs, rhs, .. } => {
19615                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19616            }
19617            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19618            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19619            Expr::Case {
19620                operand,
19621                branches,
19622                else_branch,
19623            } => {
19624                operand.as_deref().is_some_and(Self::expr_has_named_window)
19625                    || branches.iter().any(|(w, t)| {
19626                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19627                    })
19628                    || else_branch
19629                        .as_deref()
19630                        .is_some_and(Self::expr_has_named_window)
19631            }
19632            _ => false,
19633        }
19634    }
19635
19636    /// v7.39 (round 705) — the NAMES the expression references through the
19637    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19638    /// definitions nothing referenced. Traversal mirrors
19639    /// `expr_has_named_window` above.
19640    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19641        match e {
19642            Expr::WindowFunction { partition_by, .. } => {
19643                if let [Expr::Column(c)] = partition_by.as_slice()
19644                    && matches!(
19645                        c.qualifier.as_deref(),
19646                        Some("__named_window__") | Some("__named_window_ref__")
19647                    )
19648                {
19649                    into.push(c.name.clone());
19650                }
19651            }
19652            Expr::Binary { lhs, rhs, .. } => {
19653                Self::collect_named_window_refs(lhs, into);
19654                Self::collect_named_window_refs(rhs, into);
19655            }
19656            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19657                Self::collect_named_window_refs(expr, into);
19658            }
19659            Expr::FunctionCall { args, .. } => {
19660                for a in args {
19661                    Self::collect_named_window_refs(a, into);
19662                }
19663            }
19664            Expr::Case {
19665                operand,
19666                branches,
19667                else_branch,
19668            } => {
19669                if let Some(o) = operand.as_deref() {
19670                    Self::collect_named_window_refs(o, into);
19671                }
19672                for (w, t) in branches {
19673                    Self::collect_named_window_refs(w, into);
19674                    Self::collect_named_window_refs(t, into);
19675                }
19676                if let Some(eb) = else_branch.as_deref() {
19677                    Self::collect_named_window_refs(eb, into);
19678                }
19679            }
19680            _ => {}
19681        }
19682    }
19683
19684    /// Inline named-window definitions into the `OVER w` markers.
19685    /// An unknown name errors (PG: window "w" does not exist).
19686    #[allow(clippy::type_complexity)]
19687    fn substitute_named_windows(
19688        e: &mut Expr,
19689        defs: &[(
19690            String,
19691            (
19692                Vec<Expr>,
19693                Vec<(Expr, bool, Option<bool>)>,
19694                Option<WindowFrame>,
19695            ),
19696        )],
19697    ) -> Result<(), String> {
19698        match e {
19699            Expr::WindowFunction {
19700                partition_by,
19701                order_by,
19702                frame,
19703                ..
19704            } => {
19705                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19706                // from the bare `OVER w1` (a plain reference).
19707                let named = match partition_by.as_slice() {
19708                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19709                        Some("__named_window__") => Some((c.name.clone(), false)),
19710                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19711                        _ => None,
19712                    },
19713                    _ => None,
19714                };
19715                if let Some((wname, is_copy)) = named {
19716                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19717                    else {
19718                        return Err(alloc::format!("window {wname:?} does not exist"));
19719                    };
19720                    if !is_copy {
19721                        *partition_by = def.0.clone();
19722                        *order_by = def.1.clone();
19723                        *frame = def.2.clone();
19724                        return Ok(());
19725                    }
19726                    // v7.39 (round 229) — PG's copy rules, probed against
19727                    // 18.4: a copy inherits the partitioning, may supply an
19728                    // ordering only when the base has none, and may not copy
19729                    // a base that already carries a frame (its own frame
19730                    // would be ambiguous with the inherited one).
19731                    if !def.1.is_empty() && !order_by.is_empty() {
19732                        return Err(alloc::format!(
19733                            "cannot override ORDER BY clause of window \"{wname}\""
19734                        ));
19735                    }
19736                    if def.2.is_some() {
19737                        return Err(alloc::format!(
19738                            "cannot copy window \"{wname}\" because it has a frame clause"
19739                        ));
19740                    }
19741                    *partition_by = def.0.clone();
19742                    if order_by.is_empty() {
19743                        *order_by = def.1.clone();
19744                    }
19745                }
19746                Ok(())
19747            }
19748            Expr::Binary { lhs, rhs, .. } => {
19749                Self::substitute_named_windows(lhs, defs)?;
19750                Self::substitute_named_windows(rhs, defs)
19751            }
19752            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19753                Self::substitute_named_windows(expr, defs)
19754            }
19755            Expr::FunctionCall { args, .. } => {
19756                for a in args {
19757                    Self::substitute_named_windows(a, defs)?;
19758                }
19759                Ok(())
19760            }
19761            Expr::Case {
19762                operand,
19763                branches,
19764                else_branch,
19765            } => {
19766                if let Some(op) = operand {
19767                    Self::substitute_named_windows(op, defs)?;
19768                }
19769                for (w, t) in branches {
19770                    Self::substitute_named_windows(w, defs)?;
19771                    Self::substitute_named_windows(t, defs)?;
19772                }
19773                if let Some(el) = else_branch {
19774                    Self::substitute_named_windows(el, defs)?;
19775                }
19776                Ok(())
19777            }
19778            _ => Ok(()),
19779        }
19780    }
19781
19782    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19783    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19784    /// composition.
19785    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19786        debug_assert!(matches!(self.peek(), Token::Table));
19787        self.advance(); // TABLE
19788        let tname = self.expect_ident_like()?;
19789        Ok(SelectStatement {
19790            locking: None,
19791            ctes: Vec::new(),
19792            distinct: false,
19793            distinct_on: Vec::new(),
19794            items: alloc::vec![SelectItem::Wildcard],
19795            from: Some(FromClause {
19796                primary: TableRef {
19797                    name: tname,
19798                    alias: None,
19799                    only: false,
19800                    as_of_segment: None,
19801                    unnest_expr: None,
19802                    unnest_column_aliases: Vec::new(),
19803                    with_ordinality: false,
19804                    generate_series_args: None,
19805                    lateral_subquery: None,
19806                    jsonb_each_text_arg: None,
19807                    table_fn_call: None,
19808                    rows_from: None,
19809                    json_table: None,
19810                    scalar_fn_item: false,
19811                },
19812                joins: Vec::new(),
19813            }),
19814            where_: None,
19815            group_by: None,
19816            group_by_all: false,
19817            having: None,
19818            unions: Vec::new(),
19819            order_by: Vec::new(),
19820            limit: None,
19821            offset: None,
19822            limit_with_ties: false,
19823            window_check_exprs: Vec::new(),
19824        })
19825    }
19826
19827    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19828    /// variants) → a derived table that reads each declared column out of
19829    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19830    /// `jsonb_array_elements(J)` (one row per element, column `value`);
19831    /// the scalar *record form projects a single row straight off `J`.
19832    /// Rides the existing lateral-subquery channel, so no new executor or
19833    /// AST is needed.
19834    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19835        use crate::ast::{
19836            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19837        };
19838        let fn_name = match self.peek() {
19839            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19840            _ => unreachable!("caller guarded is_json_to_record_name"),
19841        };
19842        self.advance(); // fn name
19843        self.advance(); // (
19844        let mut arg = self.parse_expr(0)?;
19845        // populate_record(base, json): the base only carries the record
19846        // type here — the JSON argument is the second expression.
19847        let mut base: Option<Expr> = None;
19848        if matches!(self.peek(), Token::Comma) {
19849            self.advance();
19850            base = Some(arg);
19851            arg = self.parse_expr(0)?;
19852        }
19853        if !matches!(self.peek(), Token::RParen) {
19854            return Err(self.err(alloc::format!(
19855                "expected ')' after {fn_name}() argument, got {:?}",
19856                self.peek()
19857            )));
19858        }
19859        self.advance(); // )
19860        let is_set = fn_name.ends_with("recordset");
19861        // `[AS] alias ( col type [, …] )` column-definition list.
19862        if matches!(self.peek(), Token::As) {
19863            self.advance();
19864        }
19865        let alias_opt = match self.peek() {
19866            Token::Ident(s) | Token::QuotedIdent(s) => {
19867                let a = s.clone();
19868                self.advance();
19869                Some(a)
19870            }
19871            _ => None,
19872        };
19873        // v7.39 (read01 round 76) — the populate family's canonical PG
19874        // spelling carries no column list at all: the row shape comes from
19875        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19876        // j)`). The parser has no catalog, so hand the two arguments to the
19877        // engine's table-function channel, which does. Only `*_to_record*`
19878        // (whose base is bare `record`) genuinely requires the list.
19879        if !matches!(self.peek(), Token::LParen) {
19880            if let Some(base_expr) = base {
19881                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19882                return Ok(TableRef {
19883                    name: alias.clone(),
19884                    alias: Some(alias),
19885                    only: false,
19886                    as_of_segment: None,
19887                    unnest_expr: None,
19888                    unnest_column_aliases: Vec::new(),
19889                    with_ordinality: false,
19890                    generate_series_args: None,
19891                    lateral_subquery: None,
19892                    jsonb_each_text_arg: None,
19893                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19894                    rows_from: None,
19895                    json_table: None,
19896                    scalar_fn_item: false,
19897                });
19898            }
19899            return Err(self.err(alloc::format!(
19900                "expected '(' to start the {fn_name} column-definition list, got {:?}",
19901                self.peek()
19902            )));
19903        }
19904        let Some(alias) = alias_opt else {
19905            return Err(self.err(alloc::format!(
19906                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19907            )));
19908        };
19909        self.advance(); // (
19910        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19911        loop {
19912            let col = self.expect_ident_like()?;
19913            let ty = self.parse_cast_target()?;
19914            coldefs.push((col, ty));
19915            if matches!(self.peek(), Token::Comma) {
19916                self.advance();
19917                continue;
19918            }
19919            if matches!(self.peek(), Token::RParen) {
19920                self.advance();
19921                break;
19922            }
19923            return Err(self.err(alloc::format!(
19924                "expected ',' or ')' in {fn_name} column list, got {:?}",
19925                self.peek()
19926            )));
19927        }
19928        if coldefs.is_empty() {
19929            return Err(self.err(alloc::format!(
19930                "{fn_name} column-definition list must declare at least one column"
19931            )));
19932        }
19933        // Per column: (base ->> 'col')::type AS col. The base is the
19934        // per-element `value` column for the *set form, or the argument
19935        // itself for the scalar record form.
19936        let items: Vec<SelectItem> = coldefs
19937            .into_iter()
19938            .map(|(col, ty)| {
19939                let base = if is_set {
19940                    Expr::Column(ColumnName {
19941                        qualifier: None,
19942                        name: "value".to_string(),
19943                    })
19944                } else {
19945                    arg.clone()
19946                };
19947                SelectItem::Expr {
19948                    expr: Expr::Cast {
19949                        expr: Box::new(Expr::Binary {
19950                            lhs: Box::new(base),
19951                            op: BinOp::JsonGetText,
19952                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19953                        }),
19954                        target: ty,
19955                    },
19956                    alias: Some(col),
19957                }
19958            })
19959            .collect();
19960        let from = if is_set {
19961            let elem_fn = if fn_name.starts_with("jsonb") {
19962                "jsonb_array_elements"
19963            } else {
19964                "json_array_elements"
19965            };
19966            Some(FromClause {
19967                primary: TableRef {
19968                    name: "value".to_string(),
19969                    alias: None,
19970                    only: false,
19971                    as_of_segment: None,
19972                    unnest_expr: Some(Box::new(Expr::FunctionCall {
19973                        name: elem_fn.to_string(),
19974                        args: alloc::vec![arg],
19975                    })),
19976                    unnest_column_aliases: alloc::vec!["value".to_string()],
19977                    with_ordinality: false,
19978                    generate_series_args: None,
19979                    lateral_subquery: None,
19980                    jsonb_each_text_arg: None,
19981                    table_fn_call: None,
19982                    rows_from: None,
19983                    json_table: None,
19984                    scalar_fn_item: false,
19985                },
19986                joins: Vec::new(),
19987            })
19988        } else {
19989            None
19990        };
19991        let inner = SelectStatement {
19992            locking: None,
19993            ctes: Vec::new(),
19994            distinct: false,
19995            distinct_on: Vec::new(),
19996            items,
19997            from,
19998            where_: None,
19999            group_by: None,
20000            group_by_all: false,
20001            having: None,
20002            unions: Vec::new(),
20003            order_by: Vec::new(),
20004            limit: None,
20005            offset: None,
20006            limit_with_ties: false,
20007            window_check_exprs: Vec::new(),
20008        };
20009        Ok(TableRef {
20010            name: alias.clone(),
20011            alias: Some(alias),
20012            only: false,
20013            as_of_segment: None,
20014            unnest_expr: None,
20015            unnest_column_aliases: Vec::new(),
20016            with_ordinality: false,
20017            generate_series_args: None,
20018            lateral_subquery: Some(Box::new(inner)),
20019            jsonb_each_text_arg: None,
20020            table_fn_call: None,
20021            rows_from: None,
20022            json_table: None,
20023            scalar_fn_item: false,
20024        })
20025    }
20026
20027    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20028    /// Returns true when the clause was present. `WITH` alone (a
20029    /// CTE can never start here) is not enough — the ORDINALITY
20030    /// ident must follow, so a stray WITH still errors downstream.
20031    fn absorb_with_ordinality(&mut self) -> bool {
20032        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20033            && matches!(self.tokens.get(self.pos + 1),
20034                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20035        {
20036            self.advance();
20037            self.advance();
20038            true
20039        } else {
20040            false
20041        }
20042    }
20043
20044    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20045    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20046    /// Out-of-line: the caller sits on the FROM recursion chain.
20047    #[inline(never)]
20048    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20049        let fn_name = match self.advance() {
20050            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20051            _ => unreachable!("caller peeked an ident"),
20052        };
20053        self.advance(); // (
20054        let mut args: Vec<Expr> = Vec::new();
20055        if !matches!(self.peek(), Token::RParen) {
20056            loop {
20057                args.push(self.parse_expr(0)?);
20058                if matches!(self.peek(), Token::Comma) {
20059                    self.advance();
20060                    continue;
20061                }
20062                break;
20063            }
20064        }
20065        if !matches!(self.peek(), Token::RParen) {
20066            return Err(self.err(alloc::format!(
20067                "expected ')' after {fn_name}() arguments, got {:?}",
20068                self.peek()
20069            )));
20070        }
20071        self.advance();
20072        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20073        // counter column rides after the function's own, and the alias list
20074        // names it.
20075        let with_ordinality = self.absorb_with_ordinality();
20076        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20077        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20078        Ok(TableRef {
20079            name,
20080            alias: alias_ident,
20081            only: false,
20082            as_of_segment: None,
20083            unnest_expr: None,
20084            unnest_column_aliases,
20085            with_ordinality,
20086            generate_series_args: None,
20087            lateral_subquery: None,
20088            jsonb_each_text_arg: None,
20089            table_fn_call: Some(Box::new((fn_name, args))),
20090            rows_from: None,
20091            json_table: None,
20092            scalar_fn_item: false,
20093        })
20094    }
20095
20096    /// v7.39 (round 205, JSON_TABLE) — parse
20097    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20098    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20099    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20100    #[inline(never)]
20101    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20102        self.advance(); // json_table
20103        self.advance(); // (
20104        let doc = Box::new(self.parse_expr(0)?);
20105        self.expect_comma_json_table()?;
20106        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20107        // Optional `PASSING <expr> AS <name> [, …]`.
20108        let mut passing: Vec<(String, Expr)> = Vec::new();
20109        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20110            self.advance();
20111            loop {
20112                let e = self.parse_expr(0)?;
20113                if !matches!(self.peek(), Token::As) {
20114                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20115                }
20116                self.advance();
20117                let vname = match self.advance() {
20118                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20119                    other => {
20120                        return Err(self.err(alloc::format!(
20121                            "expected PASSING variable name, got {other:?}"
20122                        )));
20123                    }
20124                };
20125                passing.push((vname, e));
20126                if matches!(self.peek(), Token::Comma) {
20127                    self.advance();
20128                    continue;
20129                }
20130                break;
20131            }
20132        }
20133        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20134            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20135        }
20136        self.advance();
20137        let columns = self.parse_json_table_columns()?;
20138        if !matches!(self.peek(), Token::RParen) {
20139            return Err(self.err(alloc::format!(
20140                "expected ')' to close JSON_TABLE, got {:?}",
20141                self.peek()
20142            )));
20143        }
20144        self.advance();
20145        let alias_ident = self.parse_optional_alias()?;
20146        let name = alias_ident
20147            .clone()
20148            .unwrap_or_else(|| String::from("json_table"));
20149        Ok(TableRef {
20150            name,
20151            alias: alias_ident,
20152            only: false,
20153            as_of_segment: None,
20154            unnest_expr: None,
20155            unnest_column_aliases: Vec::new(),
20156            with_ordinality: false,
20157            generate_series_args: None,
20158            lateral_subquery: None,
20159            jsonb_each_text_arg: None,
20160            table_fn_call: None,
20161            rows_from: None,
20162            json_table: Some(Box::new(crate::ast::JsonTable {
20163                doc,
20164                row_path,
20165                columns,
20166                passing,
20167            })),
20168            scalar_fn_item: false,
20169        })
20170    }
20171
20172    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20173        if !matches!(self.peek(), Token::Comma) {
20174            return Err(self.err(alloc::format!(
20175                "expected ',' after JSON_TABLE document, got {:?}",
20176                self.peek()
20177            )));
20178        }
20179        self.advance();
20180        Ok(())
20181    }
20182
20183    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20184        match self.advance() {
20185            Token::String(s) => Ok(s),
20186            other => Err(self.err(alloc::format!(
20187                "expected {what} string literal, got {other:?}"
20188            ))),
20189        }
20190    }
20191
20192    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20193    #[inline(never)]
20194    fn parse_json_table_columns(
20195        &mut self,
20196    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20197        if !matches!(self.peek(), Token::LParen) {
20198            return Err(self.err("expected '(' after COLUMNS".into()));
20199        }
20200        self.advance();
20201        let mut cols = Vec::new();
20202        loop {
20203            cols.push(self.parse_json_table_one_column()?);
20204            if matches!(self.peek(), Token::Comma) {
20205                self.advance();
20206                continue;
20207            }
20208            break;
20209        }
20210        if !matches!(self.peek(), Token::RParen) {
20211            return Err(self.err(alloc::format!(
20212                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20213                self.peek()
20214            )));
20215        }
20216        self.advance();
20217        Ok(cols)
20218    }
20219
20220    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20221        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20222        // NESTED [PATH] '<p>' COLUMNS (...)
20223        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20224            self.advance();
20225            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20226                self.advance();
20227            }
20228            let path = self.parse_json_string_literal("NESTED PATH")?;
20229            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20230                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20231            }
20232            self.advance();
20233            let columns = self.parse_json_table_columns()?;
20234            return Ok(JsonTableColumn::Nested { path, columns });
20235        }
20236        // <name> ...
20237        let name = match self.advance() {
20238            Token::Ident(s) | Token::QuotedIdent(s) => s,
20239            other => {
20240                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20241            }
20242        };
20243        // <name> FOR ORDINALITY
20244        if matches!(self.peek(), Token::For) {
20245            self.advance();
20246            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20247                return Err(self.err("expected ORDINALITY after FOR".into()));
20248            }
20249            self.advance();
20250            return Ok(JsonTableColumn::Ordinality { name });
20251        }
20252        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20253        let ty = self.parse_column_type_name()?;
20254        let mut format_json = false;
20255        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20256            self.advance();
20257            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20258                return Err(self.err("expected JSON after FORMAT".into()));
20259            }
20260            self.advance();
20261            format_json = true;
20262        }
20263        let mut exists = false;
20264        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20265            self.advance();
20266            exists = true;
20267        }
20268        let mut path = alloc::format!("$.{name}");
20269        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20270            self.advance();
20271            path = self.parse_json_string_literal("column PATH")?;
20272        }
20273        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20274            // `FORMAT JSON` after PATH (alternate placement).
20275            self.advance();
20276            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20277                self.advance();
20278            }
20279            format_json = true;
20280        }
20281        let mut wrapper = false;
20282        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20283            self.advance();
20284            // optional CONDITIONAL/UNCONDITIONAL
20285            if matches!(self.peek(), Token::Ident(s)
20286                if s.eq_ignore_ascii_case("unconditional")
20287                    || s.eq_ignore_ascii_case("conditional"))
20288            {
20289                self.advance();
20290            }
20291            if !matches!(self.peek(), Token::Ident(s)
20292                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20293            {
20294                return Err(self.err("expected WRAPPER after WITH".into()));
20295            }
20296            self.advance();
20297            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20298            if matches!(self.peek(), Token::Ident(s)
20299                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20300            {
20301                self.advance();
20302            }
20303            wrapper = true;
20304        }
20305        // ON EMPTY / ON ERROR clauses (two, in any order).
20306        let mut on_empty = JsonTableOnBehavior::Null;
20307        let mut on_error = JsonTableOnBehavior::Null;
20308        for _ in 0..2 {
20309            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20310            {
20311                self.advance();
20312                Some(JsonTableOnBehavior::Error)
20313            } else if matches!(self.peek(), Token::Null) {
20314                self.advance();
20315                Some(JsonTableOnBehavior::Null)
20316            } else if matches!(self.peek(), Token::Default) {
20317                self.advance();
20318                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20319            } else {
20320                None
20321            };
20322            let Some(behavior) = behavior else { break };
20323            // `ON {EMPTY|ERROR}`
20324            if !matches!(self.peek(), Token::On) {
20325                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20326            }
20327            self.advance();
20328            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20329                self.advance();
20330                on_empty = behavior;
20331            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20332                self.advance();
20333                on_error = behavior;
20334            } else {
20335                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20336            }
20337        }
20338        Ok(JsonTableColumn::Regular {
20339            name,
20340            ty,
20341            path,
20342            exists,
20343            format_json,
20344            wrapper,
20345            on_empty,
20346            on_error,
20347        })
20348    }
20349
20350    fn parse_optional_alias_with_columns(
20351        &mut self,
20352    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20353        let alias = self.parse_optional_alias()?;
20354        if alias.is_none() {
20355            return Ok((None, Vec::new()));
20356        }
20357        let mut cols: Vec<String> = Vec::new();
20358        if matches!(self.peek(), Token::LParen) {
20359            self.advance();
20360            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20361                self.advance();
20362                cols.push(s);
20363                if matches!(self.peek(), Token::Comma) {
20364                    self.advance();
20365                    continue;
20366                }
20367                break;
20368            }
20369            if matches!(self.peek(), Token::RParen) {
20370                self.advance();
20371            }
20372        }
20373        Ok((alias, cols))
20374    }
20375
20376    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20377    /// whose keyword token was already consumed and whose `(` is the
20378    /// current token. Factored out of `parse_atom` (and marked
20379    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20380    /// recursive `parse_atom` frame — inlining them there enlarges the
20381    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20382    /// against, risking an overflow before the budget triggers.
20383    #[inline(never)]
20384    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20385        self.advance(); // (
20386        let mut args = Vec::new();
20387        if !matches!(self.peek(), Token::RParen) {
20388            loop {
20389                args.push(self.parse_expr(0)?);
20390                match self.peek() {
20391                    Token::Comma => {
20392                        self.advance();
20393                    }
20394                    Token::RParen => break,
20395                    other => {
20396                        return Err(self.err(alloc::format!(
20397                            "expected ',' or ')' in {name}() args, got {other:?}"
20398                        )));
20399                    }
20400                }
20401            }
20402        }
20403        self.advance(); // )
20404        Ok(Expr::FunctionCall {
20405            name: name.into(),
20406            args,
20407        })
20408    }
20409
20410    /// FROM-clause: a primary table reference plus zero-or-more joined
20411    /// peers expressed via either `, <table>` (cross-product, no ON) or
20412    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20413    /// v1.10 keeps the join list flat (left-associative nested-loop
20414    /// semantics).
20415    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20416        let primary = self.parse_table_ref()?;
20417        let primary_qual = primary
20418            .alias
20419            .clone()
20420            .unwrap_or_else(|| primary.name.clone());
20421        let joins = self.parse_from_joins(&primary_qual)?;
20422        Ok(FromClause { primary, joins })
20423    }
20424
20425    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20426    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20427    /// SAME grammar after its target table has already been consumed.
20428    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20429    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20430    /// be parsed forward, once.)
20431    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20432    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20433    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20434    /// desugaring, which needs a name for the left side of each equality.
20435    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20436        let mut joins = Vec::new();
20437        loop {
20438            // `, <table>` — cross-product with no ON.
20439            if matches!(self.peek(), Token::Comma) {
20440                self.advance();
20441                let table = self.parse_table_ref()?;
20442                joins.push(FromJoin {
20443                    kind: JoinKind::Cross,
20444                    table,
20445                    on: None,
20446                    using_cols: None,
20447                    natural: false,
20448                });
20449                continue;
20450            }
20451            // v7.37.16 — optional leading `NATURAL` before the join
20452            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20453            // not a lexer keyword (it arrives as a bare Ident), so match
20454            // it case-insensitively here. When present, no ON/USING
20455            // clause is allowed — the common columns are resolved at
20456            // execution time.
20457            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20458            if natural {
20459                self.advance();
20460            }
20461            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20462            // CROSS JOIN, and bare JOIN (defaults to INNER).
20463            let kind =
20464                match self.peek() {
20465                    Token::Inner => {
20466                        self.advance();
20467                        if !matches!(self.peek(), Token::Join) {
20468                            return Err(self
20469                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20470                        }
20471                        self.advance();
20472                        JoinKind::Inner
20473                    }
20474                    Token::Left => {
20475                        self.advance();
20476                        if matches!(self.peek(), Token::Outer) {
20477                            self.advance();
20478                        }
20479                        if !matches!(self.peek(), Token::Join) {
20480                            return Err(self.err(format!(
20481                                "expected JOIN after LEFT [OUTER], got {:?}",
20482                                self.peek()
20483                            )));
20484                        }
20485                        self.advance();
20486                        JoinKind::Left
20487                    }
20488                    Token::Cross => {
20489                        self.advance();
20490                        if !matches!(self.peek(), Token::Join) {
20491                            return Err(self
20492                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20493                        }
20494                        self.advance();
20495                        JoinKind::Cross
20496                    }
20497                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20498                    Token::Right => {
20499                        self.advance();
20500                        if matches!(self.peek(), Token::Outer) {
20501                            self.advance();
20502                        }
20503                        if !matches!(self.peek(), Token::Join) {
20504                            return Err(self.err(format!(
20505                                "expected JOIN after RIGHT [OUTER], got {:?}",
20506                                self.peek()
20507                            )));
20508                        }
20509                        self.advance();
20510                        JoinKind::Right
20511                    }
20512                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20513                    Token::Full => {
20514                        self.advance();
20515                        if matches!(self.peek(), Token::Outer) {
20516                            self.advance();
20517                        }
20518                        if !matches!(self.peek(), Token::Join) {
20519                            return Err(self.err(format!(
20520                                "expected JOIN after FULL [OUTER], got {:?}",
20521                                self.peek()
20522                            )));
20523                        }
20524                        self.advance();
20525                        JoinKind::FullOuter
20526                    }
20527                    Token::Join => {
20528                        self.advance();
20529                        JoinKind::Inner
20530                    }
20531                    _ => break,
20532                };
20533            let table = self.parse_table_ref()?;
20534            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20535            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20536            // where prev_table is the most-recent left-side table
20537            // (the previous join's table if any, else the FROM primary).
20538            // PG semantics around column merging are richer (USING'd
20539            // cols become deduplicated single output columns); for
20540            // sugar purposes the predicate-only form covers the
20541            // baseline corpus shape and chained `… JOIN x USING (k)
20542            // JOIN y USING (k)` calls.
20543            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20544            // common columns resolve at execution time.
20545            if natural {
20546                joins.push(FromJoin {
20547                    kind,
20548                    table,
20549                    on: None,
20550                    using_cols: None,
20551                    natural: true,
20552                });
20553                continue;
20554            }
20555            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20556            // v7.37.16 — capture the USING column list (in addition to
20557            // the ON desugar below) so the executor can perform PG's
20558            // column-merge on the output side.
20559            let mut using_cols: Option<Vec<String>> = None;
20560            let on = if matches!(self.peek(), Token::On) {
20561                self.advance();
20562                Some(self.parse_expr(0)?)
20563            } else if using_match {
20564                self.advance();
20565                if !matches!(self.peek(), Token::LParen) {
20566                    return Err(
20567                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20568                    );
20569                }
20570                self.advance();
20571                let mut cols: Vec<String> = Vec::new();
20572                loop {
20573                    match self.peek().clone() {
20574                        Token::Ident(s) | Token::QuotedIdent(s) => {
20575                            self.advance();
20576                            cols.push(s);
20577                        }
20578                        other => {
20579                            return Err(self.err(format!(
20580                                "expected column name inside USING (…), got {other:?}"
20581                            )));
20582                        }
20583                    }
20584                    match self.peek() {
20585                        Token::Comma => {
20586                            self.advance();
20587                            continue;
20588                        }
20589                        Token::RParen => {
20590                            self.advance();
20591                            break;
20592                        }
20593                        other => {
20594                            return Err(self.err(format!(
20595                                "expected ',' or ')' inside USING (…), got {other:?}"
20596                            )));
20597                        }
20598                    }
20599                }
20600                if cols.is_empty() {
20601                    return Err(self.err("USING (…) requires at least one column".to_string()));
20602                }
20603                using_cols = Some(cols.clone());
20604                // Pick the left-side alias: prev join's table if any,
20605                // else FROM primary. Use alias when present, else
20606                // table name (PG-equivalent qualifier).
20607                let left_qual: String = joins
20608                    .last()
20609                    .map(|j| {
20610                        j.table
20611                            .alias
20612                            .clone()
20613                            .unwrap_or_else(|| j.table.name.clone())
20614                    })
20615                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20616                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20617                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20618                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20619                        qualifier: Some(left_qual.clone()),
20620                        name: c.clone(),
20621                    })),
20622                    op: crate::ast::BinOp::Eq,
20623                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20624                        qualifier: Some(right_qual.clone()),
20625                        name: c,
20626                    })),
20627                });
20628                let first = iter.next().expect("at least one col");
20629                Some(iter.fold(first, |acc, pred| Expr::Binary {
20630                    lhs: alloc::boxed::Box::new(acc),
20631                    op: crate::ast::BinOp::And,
20632                    rhs: alloc::boxed::Box::new(pred),
20633                }))
20634            } else if kind == JoinKind::Cross {
20635                None
20636            } else {
20637                return Err(self.err(format!(
20638                    "expected ON or USING after {:?} JOIN, got {:?}",
20639                    kind,
20640                    self.peek()
20641                )));
20642            };
20643            joins.push(FromJoin {
20644                kind,
20645                table,
20646                on,
20647                using_cols,
20648                natural: false,
20649            });
20650        }
20651        Ok(joins)
20652    }
20653
20654    /// Optional alias after an expression or table:
20655    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20656    /// accepted (PG-style implicit alias). Returns `None` if the next token
20657    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20658    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20659        if matches!(self.peek(), Token::As) {
20660            self.advance();
20661            // v7.39 (round 340, V56) — after AS the next token MUST be an
20662            // identifier. This used to return None and "let the caller
20663            // surface the error on the next expectation", but when AS is
20664            // the LAST token there is no next expectation: `SELECT 1 AS`
20665            // parsed clean and silently dropped the alias. PG rejects it.
20666            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20667                return self.expect_ident_like().map(Some);
20668            }
20669            return Err(self.err(alloc::format!(
20670                "expected an alias after AS, got {:?}",
20671                self.peek()
20672            )));
20673        }
20674        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20675        // grammar reserves a long list of follow-keywords from the
20676        // alias slot. SPG's bareword approximation: skip a small
20677        // set of idents that would otherwise be swallowed as the
20678        // table alias and break trailing clauses like CREATE
20679        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20680        // CONFLICT WHERE shapes.
20681        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20682            if is_alias_stopword(s) {
20683                return Ok(None);
20684            }
20685            return Ok(self.expect_ident_like().ok());
20686        }
20687        Ok(None)
20688    }
20689
20690    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20691    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20692        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20693        // error beats a stack overflow (an overflow aborts the
20694        // embedding host process).
20695        self.enter_nested()?;
20696        let r = self.parse_expr_inner(min_prec);
20697        self.nest_depth -= 1;
20698        r
20699    }
20700
20701    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20702    /// When the upcoming tokens form one, return the underlying
20703    /// operator token and the position just past the closing paren
20704    /// so the binary loop can dispatch on the plain operator.
20705    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20706        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20707            return None;
20708        }
20709        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20710            return None;
20711        }
20712        let mut i = self.pos + 2;
20713        // Optional schema qualifier (pg_catalog.<op> etc.).
20714        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20715            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20716        {
20717            i += 2;
20718        }
20719        let op_tok = self.tokens.get(i)?.clone();
20720        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20721            return None;
20722        }
20723        Some((i + 2, op_tok))
20724    }
20725
20726    /// PG operator symbols that lower onto function calls in
20727    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20728    /// family → regexp_like, comparison rung), `^@` (starts_with,
20729    /// comparison rung), `^` (power, tighter than `*`), `#`
20730    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20731    /// subset of the OR bits so the subtraction never borrows).
20732    fn try_symbol_operator(
20733        &mut self,
20734        lhs: &Expr,
20735        min_prec: u8,
20736    ) -> Result<Option<Expr>, ParseError> {
20737        enum Sym {
20738            Regex { ci: bool, negated: bool },
20739            Like { ci: bool, negated: bool },
20740            StartsWith,
20741            Power,
20742            Xor,
20743            RangeAdjacent,
20744        }
20745        // v7.39 (IS-precedence knife) — the low-precedence postfix
20746        // predicates ride this existing leaf call (zero new frame slots
20747        // on the nesting chain).
20748        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20749            return Ok(Some(e));
20750        }
20751        let (sym, prec): (Sym, u8) = match self.peek() {
20752            Token::Tilde => (
20753                Sym::Regex {
20754                    ci: false,
20755                    negated: false,
20756                },
20757                5,
20758            ),
20759            Token::TildeStar => (
20760                Sym::Regex {
20761                    ci: true,
20762                    negated: false,
20763                },
20764                5,
20765            ),
20766            Token::NotTilde => (
20767                Sym::Regex {
20768                    ci: false,
20769                    negated: true,
20770                },
20771                5,
20772            ),
20773            Token::NotTildeStar => (
20774                Sym::Regex {
20775                    ci: true,
20776                    negated: true,
20777                },
20778                5,
20779            ),
20780            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20781            Token::DoubleTilde => (
20782                Sym::Like {
20783                    ci: false,
20784                    negated: false,
20785                },
20786                5,
20787            ),
20788            Token::DoubleTildeStar => (
20789                Sym::Like {
20790                    ci: true,
20791                    negated: false,
20792                },
20793                5,
20794            ),
20795            Token::NotDoubleTilde => (
20796                Sym::Like {
20797                    ci: false,
20798                    negated: true,
20799                },
20800                5,
20801            ),
20802            Token::NotDoubleTildeStar => (
20803                Sym::Like {
20804                    ci: true,
20805                    negated: true,
20806                },
20807                5,
20808            ),
20809            Token::CaretAt => (Sym::StartsWith, 5),
20810            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20811            // tighter than `* / & |`, which the prec-9 rung preserves —
20812            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20813            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20814            Token::Caret => (Sym::Power, 9),
20815            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20816            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20817            Token::Hash => (Sym::Xor, 6),
20818            Token::Adjacent => (Sym::RangeAdjacent, 5),
20819            _ => return Ok(None),
20820        };
20821        if prec < min_prec {
20822            return Ok(None);
20823        }
20824        self.advance();
20825        let rhs = self.parse_expr(prec + 1)?;
20826        let out = match sym {
20827            Sym::Regex { ci, negated } => {
20828                let mut args = alloc::vec![lhs.clone(), rhs];
20829                if ci {
20830                    args.push(Expr::Literal(Literal::String(String::from("i"))));
20831                }
20832                maybe_not(
20833                    Expr::FunctionCall {
20834                        name: String::from("regexp_like"),
20835                        args,
20836                    },
20837                    negated,
20838                )
20839            }
20840            Sym::Like { ci, negated } => Expr::Like {
20841                expr: alloc::boxed::Box::new(lhs.clone()),
20842                pattern: alloc::boxed::Box::new(rhs),
20843                negated,
20844                case_insensitive: ci,
20845            },
20846            Sym::StartsWith => Expr::FunctionCall {
20847                name: String::from("starts_with"),
20848                args: alloc::vec![lhs.clone(), rhs],
20849            },
20850            Sym::Power => Expr::FunctionCall {
20851                name: String::from("power"),
20852                args: alloc::vec![lhs.clone(), rhs],
20853            },
20854            // `#` bitwise XOR — a real operator now (was desugared to
20855            // `(a|b)-(a&b)`, algebraically identical for integers but
20856            // undefined for bit strings; the direct op handles both).
20857            Sym::Xor => Expr::Binary {
20858                lhs: Box::new(lhs.clone()),
20859                op: BinOp::BitXor,
20860                rhs: Box::new(rhs),
20861            },
20862            // range `-|-` "is adjacent to" — lowered to a catalog function.
20863            Sym::RangeAdjacent => Expr::FunctionCall {
20864                name: String::from("range_adjacent"),
20865                args: alloc::vec![lhs.clone(), rhs],
20866            },
20867        };
20868        Ok(Some(out))
20869    }
20870
20871    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20872    /// predicates, moved out of the tight postfix-cast loop: PG binds
20873    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20874    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20875    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20876    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20877    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20878    /// when nothing at this position belongs to the family. Out-of-line
20879    /// (`inline(never)`): the caller sits on the per-nesting-level frame
20880    /// chain that MAX_NEST_DEPTH is tuned against.
20881    #[inline(never)]
20882    fn parse_postfix_predicate(
20883        &mut self,
20884        lhs: &Expr,
20885        min_prec: u8,
20886    ) -> Result<Option<Expr>, ParseError> {
20887        // Reached through try_symbol_operator (an existing leaf call of
20888        // the binary loop) so NO new stack slots land on the per-nesting
20889        // frame chain; the lhs clones only when a predicate actually
20890        // consumes it.
20891        match self.peek() {
20892            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20893            // comparison family rung 5 (each +1 from the pre-XOR ladder).
20894            Token::Is if min_prec <= 4 => {}
20895            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20896            Token::Not
20897                if min_prec <= 5
20898                    && matches!(
20899                        self.tokens.get(self.pos + 1),
20900                        Some(Token::Between | Token::In | Token::Like)
20901                    ) => {}
20902            Token::Not | Token::Ident(_)
20903                if min_prec <= 5
20904                    && (matches!(self.peek(), Token::Ident(s)
20905                            if s.eq_ignore_ascii_case("ilike")
20906                                || (self.mysql_dialect
20907                                    && (s.eq_ignore_ascii_case("regexp")
20908                                        || s.eq_ignore_ascii_case("rlike")))
20909                                || (s.eq_ignore_ascii_case("similar")
20910                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20911                        || (matches!(self.peek(), Token::Not)
20912                            && matches!(self.tokens.get(self.pos + 1),
20913                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20914                                    || (self.mysql_dialect
20915                                        && (s.eq_ignore_ascii_case("regexp")
20916                                            || s.eq_ignore_ascii_case("rlike")))
20917                                    || s.eq_ignore_ascii_case("similar")))) => {}
20918            _ => return Ok(None),
20919        }
20920        let mut expr = lhs.clone();
20921        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20922        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20923        if min_prec <= 4 {
20924            if matches!(self.peek(), Token::Is) {
20925                self.advance();
20926                let negated = if matches!(self.peek(), Token::Not) {
20927                    self.advance();
20928                    true
20929                } else {
20930                    false
20931                };
20932                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20933                // mailrs pg_dump.
20934                if matches!(self.peek(), Token::Distinct) {
20935                    self.advance();
20936                    if !matches!(self.peek(), Token::From) {
20937                        return Err(self.err(format!(
20938                            "expected FROM after IS{} DISTINCT, got {:?}",
20939                            if negated { " NOT" } else { "" },
20940                            self.peek()
20941                        )));
20942                    }
20943                    self.advance();
20944                    // Right-hand side: parse at the same precedence
20945                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20946                    // groups as `x IS DISTINCT FROM (a + b)`.
20947                    let rhs = self.parse_expr(5)?;
20948                    let op = if negated {
20949                        BinOp::IsNotDistinctFrom
20950                    } else {
20951                        BinOp::IsDistinctFrom
20952                    };
20953                    expr = Expr::Binary {
20954                        op,
20955                        lhs: Box::new(expr),
20956                        rhs: Box::new(rhs),
20957                    };
20958                    {
20959                        return Ok(Some(expr));
20960                    }
20961                }
20962                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20963                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20964                // Lowers onto pg_is_json(x, kind); NOT wraps the
20965                // call in a logical negation.
20966                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20967                if s.eq_ignore_ascii_case("json"))
20968                {
20969                    self.advance(); // JSON
20970                    let kind = match self.peek() {
20971                        Token::Ident(s) | Token::QuotedIdent(s)
20972                            if matches!(
20973                                s.to_ascii_lowercase().as_str(),
20974                                "value" | "object" | "array" | "scalar"
20975                            ) =>
20976                        {
20977                            let k = s.to_ascii_lowercase();
20978                            self.advance();
20979                            k
20980                        }
20981                        _ => "value".to_string(),
20982                    };
20983                    let call = Expr::FunctionCall {
20984                        name: "pg_is_json".to_string(),
20985                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20986                    };
20987                    expr = if negated {
20988                        Expr::Unary {
20989                            op: UnOp::Not,
20990                            expr: Box::new(call),
20991                        }
20992                    } else {
20993                        call
20994                    };
20995                    {
20996                        return Ok(Some(expr));
20997                    }
20998                }
20999                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21000                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21001                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21002                {
21003                    let form_kw = match self.peek() {
21004                        Token::Ident(s) | Token::QuotedIdent(s)
21005                            if matches!(
21006                                s.to_ascii_uppercase().as_str(),
21007                                "NFC" | "NFD" | "NFKC" | "NFKD"
21008                            ) && matches!(
21009                                self.tokens.get(self.pos + 1),
21010                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21011                                    if n.eq_ignore_ascii_case("normalized")
21012                            ) =>
21013                        {
21014                            Some(s.to_ascii_uppercase())
21015                        }
21016                        _ => None,
21017                    };
21018                    let bare_normalized = form_kw.is_none()
21019                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21020                        if s.eq_ignore_ascii_case("normalized"));
21021                    if form_kw.is_some() || bare_normalized {
21022                        if form_kw.is_some() {
21023                            self.advance(); // form keyword
21024                        }
21025                        self.advance(); // NORMALIZED
21026                        let mut args = alloc::vec![expr];
21027                        if let Some(f) = form_kw {
21028                            args.push(Expr::Literal(Literal::String(f)));
21029                        }
21030                        let call = Expr::FunctionCall {
21031                            name: "is_normalized".to_string(),
21032                            args,
21033                        };
21034                        expr = if negated {
21035                            Expr::Unary {
21036                                op: UnOp::Not,
21037                                expr: Box::new(call),
21038                            }
21039                        } else {
21040                            call
21041                        };
21042                        {
21043                            return Ok(Some(expr));
21044                        }
21045                    }
21046                }
21047                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21048                // three-valued boolean tests. IS TRUE/FALSE never
21049                // return NULL, so they lower to CASE forms whose
21050                // ELSE catches the NULL branch; IS UNKNOWN on a
21051                // boolean is exactly IS NULL.
21052                if matches!(self.peek(), Token::True | Token::False)
21053                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21054                {
21055                    let tok = self.advance();
21056                    let test = match tok {
21057                        Token::True => Some(true),
21058                        Token::False => Some(false),
21059                        _ => None, // UNKNOWN
21060                    };
21061                    // v7.39 (round 328, V45) — kept as what the user
21062                    // wrote. These used to be lowered here into `CASE` /
21063                    // `IS NULL`; the semantics were right but the AST no
21064                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21065                    // was echoed back as
21066                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21067                    expr = Expr::BoolTest {
21068                        expr: Box::new(expr),
21069                        value: test,
21070                        negated,
21071                    };
21072                    {
21073                        return Ok(Some(expr));
21074                    }
21075                }
21076                if !matches!(self.peek(), Token::Null) {
21077                    return Err(self.err(format!(
21078                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21079                    if negated { " NOT" } else { "" },
21080                    self.peek()
21081                )));
21082                }
21083                self.advance();
21084                expr = Expr::IsNull {
21085                    expr: Box::new(expr),
21086                    negated,
21087                };
21088                {
21089                    return Ok(Some(expr));
21090                }
21091            }
21092        }
21093        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21094        if min_prec <= 5 {
21095            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21096            // Look one token ahead so a stray `NOT` not followed by any of
21097            // these flows through to the early return below untouched.
21098            let negated = if matches!(self.peek(), Token::Not) {
21099                let next = self.tokens.get(self.pos + 1);
21100                matches!(next, Some(Token::Between | Token::In | Token::Like))
21101                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21102                    || (self.mysql_dialect
21103                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21104                    || s.eq_ignore_ascii_case("similar"))
21105            } else {
21106                false
21107            };
21108            if negated {
21109                self.advance();
21110            }
21111            if matches!(self.peek(), Token::Between) {
21112                expr = self.parse_between_tail(expr, negated)?;
21113                {
21114                    return Ok(Some(expr));
21115                }
21116            }
21117            if matches!(self.peek(), Token::In) {
21118                if self.suppress_in_tail && !negated {
21119                    // POSITION(sub IN str) — IN belongs to the
21120                    // enclosing function syntax; stop here.
21121                    {
21122                        return Ok(None);
21123                    }
21124                }
21125                expr = self.parse_in_tail(expr, negated)?;
21126                {
21127                    return Ok(Some(expr));
21128                }
21129            }
21130            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21131            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21132            // (the SQL→regex transform runs inside, in the backtracking-
21133            // friendly shape SPG's matcher needs).
21134            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21135                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21136            {
21137                self.advance(); // SIMILAR
21138                self.advance(); // TO
21139                let pattern = self.parse_expr(6)?;
21140                let mut args = alloc::vec![expr, pattern];
21141                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21142                    self.advance();
21143                    args.push(self.parse_expr(6)?);
21144                }
21145                let call = Expr::FunctionCall {
21146                    name: "__similar_to".to_string(),
21147                    args,
21148                };
21149                expr = maybe_not(call, negated);
21150                {
21151                    return Ok(Some(expr));
21152                }
21153            }
21154            if matches!(self.peek(), Token::Like) {
21155                self.advance();
21156                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21157                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21158                    expr = q;
21159                    {
21160                        return Ok(Some(expr));
21161                    }
21162                }
21163                // Pattern at the same precedence as other comparison RHSes —
21164                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21165                let mut pattern = self.parse_expr(6)?;
21166                // `ESCAPE 'c'` — rewrite a literal pattern to the
21167                // default backslash escape at parse time. Custom
21168                // escapes on non-literal patterns would need
21169                // matcher support; error honestly.
21170                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21171                    self.advance();
21172                    let esc = self.parse_expr(6)?;
21173                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21174                }
21175                expr = Expr::Like {
21176                    expr: Box::new(expr),
21177                    pattern: Box::new(pattern),
21178                    negated,
21179                    case_insensitive: false,
21180                };
21181                {
21182                    return Ok(Some(expr));
21183                }
21184            }
21185            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21186            // keyword reaches us as a plain identifier.
21187            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21188                self.advance();
21189                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21190                    expr = q;
21191                    {
21192                        return Ok(Some(expr));
21193                    }
21194                }
21195                let pattern = self.parse_expr(6)?;
21196                expr = Expr::Like {
21197                    expr: Box::new(expr),
21198                    pattern: Box::new(pattern),
21199                    negated,
21200                    case_insensitive: true,
21201                };
21202                {
21203                    return Ok(Some(expr));
21204                }
21205            }
21206            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21207            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21208            // matches case-insensitively under the default collation, so it
21209            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21210            // `~*` operator uses, wrapped in NOT when negated.
21211            if self.mysql_dialect
21212                && matches!(self.peek(), Token::Ident(s)
21213                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21214            {
21215                self.advance();
21216                let pattern = self.parse_expr(6)?;
21217                let call = Expr::FunctionCall {
21218                    name: String::from("regexp_like"),
21219                    args: alloc::vec![
21220                        expr,
21221                        pattern,
21222                        Expr::Literal(Literal::String(String::from("i"))),
21223                    ],
21224                };
21225                return Ok(Some(maybe_not(call, negated)));
21226            }
21227        }
21228        let _ = expr;
21229        Ok(None)
21230    }
21231
21232    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21233        let mut lhs = self.parse_unary()?;
21234        let mut chain_len = 0usize;
21235        loop {
21236            // OPERATOR([schema.]op) reduces to its underlying
21237            // operator token before the normal dispatch.
21238            let explicit = self.peek_explicit_operator();
21239            let dispatch = match &explicit {
21240                Some((_, tok)) => self.binop_here(tok),
21241                None => self.binop_here(self.peek()),
21242            };
21243            let Some((op, prec)) = dispatch else {
21244                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21245                // of the symbol family. `binop_here` answers None for them
21246                // because they lower onto function calls rather than a
21247                // BinOp, and the fallback below reads `self.peek()` — the
21248                // word OPERATOR, not the operator. `pg_dump` writes every
21249                // catalog predicate this way, so its first query failed
21250                // and no dump ran:
21251                //
21252                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21253                //
21254                // Collapsing the wrapper to the operator it names puts the
21255                // token where the fallback already looks.
21256                if let Some((next, op_tok)) = explicit {
21257                    self.tokens.splice(self.pos..next, [op_tok]);
21258                }
21259                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21260                    lhs = e;
21261                    chain_len += 1;
21262                    if chain_len > MAX_BINARY_CHAIN {
21263                        return Err(self.err(alloc::format!(
21264                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21265                        )));
21266                    }
21267                    continue;
21268                }
21269                break;
21270            };
21271            if prec < min_prec {
21272                break;
21273            }
21274            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21275            // iteratively but evaluates and drops recursively;
21276            // depth beyond the budget overflows worker stacks.
21277            chain_len += 1;
21278            if chain_len > MAX_BINARY_CHAIN {
21279                return Err(self.err(alloc::format!(
21280                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21281                )));
21282            }
21283            match explicit {
21284                Some((end_pos, _)) => self.pos = end_pos,
21285                None => {
21286                    self.advance();
21287                }
21288            }
21289            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21290            // ANY is a bare ident; ALL is a reserved Token. Both
21291            // require an immediate `(` to disambiguate from
21292            // identifier columns named `any` / `all`.
21293            let any_kind = match self.peek() {
21294                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21295                    Some(false)
21296                }
21297                Token::Ident(s) | Token::QuotedIdent(s)
21298                    if (s.eq_ignore_ascii_case("any")
21299                        || s.eq_ignore_ascii_case("some")
21300                        || s.eq_ignore_ascii_case("all"))
21301                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21302                {
21303                    Some(!s.eq_ignore_ascii_case("all"))
21304                }
21305                _ => None,
21306            };
21307            if let Some(is_any) = any_kind {
21308                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21309                continue;
21310            }
21311            let rhs = self.parse_expr(prec + 1)?;
21312            lhs = Expr::Binary {
21313                lhs: Box::new(lhs),
21314                op,
21315                rhs: Box::new(rhs),
21316            };
21317        }
21318        Ok(lhs)
21319    }
21320
21321    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21322    /// and the array form.
21323    ///
21324    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21325    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21326    /// this block's `Expr` temporaries and four `format!` sites slots in
21327    /// that frame on every level of `((((1))))`, which never reaches it.
21328    #[inline(never)]
21329    fn parse_any_all_rhs(
21330        &mut self,
21331        lhs: Expr,
21332        op: BinOp,
21333        is_any: bool,
21334    ) -> Result<Expr, ParseError> {
21335        self.advance(); // ident
21336        self.advance(); // (
21337        // `x op ANY (SELECT …)` — the quantified-subquery
21338        // form. `= ANY` is exactly IN; the other operators
21339        // lower onto EXISTS over the subquery as a derived
21340        // table, comparing against its single projection
21341        // aliased __v (x's columns resolve correlated).
21342        // ALL is the negated-EXISTS complement; a NULL
21343        // element makes PG return NULL where this lowering
21344        // returns true — the NOT NULL column case (the
21345        // practical one) is exact.
21346        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21347            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21348            // legal PG too (round-151 sibling). Out-of-line
21349            // (#[inline(never)] helper) — this sits on
21350            // parse_expr's recursive frame and the two-armed
21351            // SELECT temporary blew the nesting-budget stack.
21352            let mut sub = self.parse_any_all_select_body()?;
21353            if !matches!(self.peek(), Token::RParen) {
21354                return Err(self.err(alloc::format!(
21355                    "expected ')' after ANY/ALL subquery, got {:?}",
21356                    self.peek()
21357                )));
21358            }
21359            self.advance();
21360            if sub.items.len() != 1 {
21361                return Err(self.err(alloc::format!(
21362                    "ANY/ALL subquery must return one column, got {}",
21363                    sub.items.len()
21364                )));
21365            }
21366            if is_any && matches!(op, BinOp::Eq) {
21367                return Ok(Expr::InSubquery {
21368                    expr: Box::new(lhs),
21369                    subquery: Box::new(sub),
21370                    negated: false,
21371                });
21372            }
21373            // The engine's subquery resolvers materialise
21374            // the single-column result into an ARRAY the
21375            // existing AnyAll three-valued eval consumes.
21376            return Ok(Expr::AnyAll {
21377                expr: Box::new(lhs),
21378                op,
21379                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21380                is_any,
21381            });
21382        }
21383        let arr = self.parse_expr(0)?;
21384        if !matches!(self.peek(), Token::RParen) {
21385            return Err(self.err(alloc::format!(
21386                "expected ')' after ANY/ALL argument, got {:?}",
21387                self.peek()
21388            )));
21389        }
21390        self.advance();
21391        Ok(Expr::AnyAll {
21392            expr: Box::new(lhs),
21393            op,
21394            array: Box::new(arr),
21395            is_any,
21396        })
21397    }
21398
21399    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21400    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21401    #[inline(never)]
21402    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21403        self.advance();
21404        let e = self.parse_expr(9)?;
21405        Ok(build_center_call(e))
21406    }
21407
21408    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21409    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21410    /// unary minus.
21411    ///
21412    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21413    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21414    /// the Expr-sized local stays out of that frame.
21415    #[inline(never)]
21416    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21417        self.advance();
21418        let e = self.parse_expr(9)?;
21419        Ok(Expr::FunctionCall {
21420            name: alloc::string::String::from(name),
21421            args: alloc::vec![e],
21422        })
21423    }
21424
21425    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21426    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21427    #[inline(never)]
21428    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21429        self.advance();
21430        let e = self.parse_expr(9)?;
21431        Ok(Expr::FunctionCall {
21432            name: alloc::string::String::from(if vertical {
21433                "isvertical"
21434            } else {
21435                "ishorizontal"
21436            }),
21437            args: alloc::vec![e],
21438        })
21439    }
21440
21441    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21442    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21443    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21444    #[inline(never)]
21445    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21446        self.advance();
21447        let e = self.parse_expr(9)?;
21448        Ok(Expr::Cast {
21449            expr: Box::new(e),
21450            target: CastTarget::Named("binary".to_string()),
21451        })
21452    }
21453
21454    /// The prefix operators that share one shape: take the token, parse
21455    /// an operand at `prec`, wrap it.
21456    ///
21457    /// `#[inline(never)]`, and one function instead of five arms, for the
21458    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21459    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21460    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21461    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21462    /// five `Expr`-sized locals per level for them anyway.
21463    #[inline(never)]
21464    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21465        self.advance();
21466        let e = self.parse_expr(prec)?;
21467        Ok(Expr::Unary {
21468            op,
21469            expr: Box::new(e),
21470        })
21471    }
21472
21473    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21474    /// and separate from it because of the literal folding below and the
21475    /// `format!` temporaries that folding needs.
21476    #[inline(never)]
21477    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21478        self.advance();
21479        // v7.39 (round 549) — fold the sign into an integer literal that
21480        // only fits once it is negative.
21481        //
21482        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21483        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21484        // folds the sign first, so `-9223372036854775808` is a bigint
21485        // there — and `-9223372036854775808 - 1` raises "bigint out of
21486        // range" where SPG quietly answered -9223372036854775809, a value
21487        // no bigint can hold. The arithmetic itself was already checked;
21488        // only the literal's type was wrong.
21489        if let Token::Numeric(lit) = self.peek()
21490            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21491        {
21492            self.advance();
21493            return Ok(Expr::Literal(Literal::Integer(folded)));
21494        }
21495        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21496        // `<->` slotted into 5 and arithmetic shifted up).
21497        let e = self.parse_expr(9)?;
21498        Ok(Expr::Unary {
21499            op: UnOp::Neg,
21500            expr: Box::new(e),
21501        })
21502    }
21503
21504    /// tsquery `!!` prefix negation, lowered to the catalog function.
21505    /// Binds like unary minus. Out-of-line for the frame reason on
21506    /// `parse_unary_op`.
21507    #[inline(never)]
21508    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21509        self.advance();
21510        let e = self.parse_expr(9)?;
21511        Ok(Expr::FunctionCall {
21512            name: String::from("tsquery_not"),
21513            args: alloc::vec![e],
21514        })
21515    }
21516
21517    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21518        match self.peek() {
21519            // NOT binds tighter than AND / XOR / OR but looser than
21520            // comparisons — its operand takes everything ≥ the comparison
21521            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21522            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21523            // was rung 3, behaviour-identical when 3 was unused; AND now
21524            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21525            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21526            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21527            // The body is out-of-line: `parse_unary` is one of the three
21528            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21529            // inline arm here overflowed the native stack in
21530            // `nesting_budget_errors_cleanly` — the guard test caught it,
21531            // exactly as the eval-side cliff did in rounds 346 and 351.
21532            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21533                self.parse_binary_prefix()
21534            }
21535            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21536            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21537            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21538            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21539            Token::Minus => self.parse_prefix_minus(),
21540            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21541            // worked only because the lexer reads it as one signed literal;
21542            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21543            // PG18 and MariaDB take all of them. Binds like unary minus.
21544            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21545            // Bitwise NOT binds like unary minus.
21546            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21547            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21548            // "center of" operator; desugars to center(x). The whole arm
21549            // is out-of-line: parse_unary sits on the per-nesting-level
21550            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21551            // Expr-sized local may live in this frame.
21552            Token::TsMatch => self.parse_prefix_center(),
21553            // v7.39 (round 508) — the prefix operators that are named
21554            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21555            // is length. Out-of-line for the same nesting-frame reason as
21556            // parse_prefix_center — parse_unary sits on the recursive cycle
21557            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21558            // live in this frame.
21559            Token::At => self.parse_prefix_call("abs"),
21560            Token::Hash => self.parse_prefix_call("npoints"),
21561            Token::AtMinusAt => self.parse_prefix_call("length"),
21562            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21563            // "is horizontal" (lseg / line); desugars to the existing
21564            // isvertical()/ishorizontal() functions. Out-of-line for the
21565            // same nesting-frame reason as parse_prefix_center.
21566            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21567            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21568            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21569            _ => self.parse_atom(),
21570        }
21571    }
21572
21573    /// Parse a parenthesised scalar subquery body after the caller has consumed
21574    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21575    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21576    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21577    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21578    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21579    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21580    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21581    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21582    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21583    /// tips the deep-nesting test into a stack overflow).
21584    #[inline(never)]
21585    fn array_subquery_ahead(&self) -> bool {
21586        if !matches!(self.peek(), Token::LParen) {
21587            return false;
21588        }
21589        matches!(
21590            self.tokens.get(self.pos + 1),
21591            Some(Token::Select | Token::Values)
21592        ) || matches!(
21593            self.tokens.get(self.pos + 1),
21594            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21595        )
21596    }
21597
21598    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21599    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21600    /// locals stay off parse_atom's recursive frame (round 105).
21601    #[inline(never)]
21602    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21603        self.advance(); // consume `[`
21604        let mut items: Vec<Expr> = Vec::new();
21605        if !matches!(self.peek(), Token::RBracket) {
21606            loop {
21607                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21608                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21609                if matches!(self.peek(), Token::LBracket) {
21610                    items.push(self.parse_array_bracket_body()?);
21611                } else {
21612                    items.push(self.parse_expr(0)?);
21613                }
21614                match self.peek() {
21615                    Token::Comma => {
21616                        self.advance();
21617                    }
21618                    Token::RBracket => break,
21619                    other => {
21620                        return Err(self.err(alloc::format!(
21621                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21622                        )));
21623                    }
21624                }
21625            }
21626        }
21627        self.advance(); // consume `]`
21628        Ok(Expr::Array(items))
21629    }
21630
21631    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21632    /// is already consumed; the current token is `(`. Desugars to a scalar
21633    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21634    /// the subquery's single-column rows in order — reusing the existing
21635    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21636    /// keeps the large `Statement` local off parse_atom's recursive frame.
21637    #[inline(never)]
21638    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21639        self.advance(); // consume `(`
21640        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21641            if w.eq_ignore_ascii_case("with"));
21642        let sub = if is_with {
21643            self.advance(); // WITH
21644            self.parse_with_cte_then_select()?
21645        } else {
21646            self.parse_select_stmt()?
21647        };
21648        if !matches!(self.peek(), Token::RParen) {
21649            return Err(self.err(alloc::format!(
21650                "expected ')' to close ARRAY(subquery), got {:?}",
21651                self.peek()
21652            )));
21653        }
21654        self.advance(); // consume `)`
21655        // Reuse the parser to build the array_agg wrapper from the subquery's
21656        // canonical text — avoids hand-constructing the derived-table AST.
21657        let wrapper = alloc::format!(
21658            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21659        );
21660        let stmt = parse_statement(&wrapper)
21661            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21662        let Statement::Select(sel) = stmt else {
21663            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21664        };
21665        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21666    }
21667
21668    #[inline(never)]
21669    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21670        let inner = if is_with {
21671            self.advance(); // WITH
21672            self.parse_with_cte_then_select()?
21673        } else {
21674            self.parse_select_stmt()?
21675        };
21676        match self.advance() {
21677            Token::RParen => {
21678                let Statement::Select(s) = inner else {
21679                    return Err(ParseError {
21680                        message: "scalar subquery body must be a SELECT".into(),
21681                        token_pos: self.consumed_pos(),
21682                    });
21683                };
21684                Ok(Expr::ScalarSubquery(Box::new(s)))
21685            }
21686            other => Err(ParseError {
21687                message: format!("expected ')' after scalar subquery, got {other:?}"),
21688                token_pos: self.consumed_pos(),
21689            }),
21690        }
21691    }
21692
21693    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21694    /// literals. The lexer splits them into an ident + string; recombine
21695    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21696    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21697    /// frame for the `body` / `bits` strings and their char loops (the
21698    /// round-367 frame cliff, M20).
21699    #[inline(never)]
21700    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21701        let is_hex = match self.peek() {
21702            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21703            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21704            _ => return None,
21705        };
21706        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21707            return None;
21708        }
21709        self.advance();
21710        let Token::String(body) = self.advance() else {
21711            unreachable!("guarded above");
21712        };
21713        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21714        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21715        // (hex pairs, even count required — MariaDB errors on an odd
21716        // count); `b'1010'` packs its bits big-endian, left-padded to a
21717        // byte. Lower both onto the bytea cast.
21718        if self.mysql_dialect {
21719            if is_hex {
21720                if body.len() % 2 == 1 {
21721                    return Some(Err(self.err(alloc::format!(
21722                        "invalid hex string literal X'{body}': odd digit count"
21723                    ))));
21724                }
21725                for c in body.chars() {
21726                    if !c.is_ascii_hexdigit() {
21727                        return Some(Err(
21728                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21729                        ));
21730                    }
21731                }
21732                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21733            }
21734            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21735                return Some(Err(
21736                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21737                ));
21738            }
21739            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21740        }
21741        let bits = if is_hex {
21742            let mut out = String::with_capacity(body.len() * 4);
21743            for c in body.chars() {
21744                let Some(d) = c.to_digit(16) else {
21745                    return Some(Err(self.err(alloc::format!(
21746                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21747                    ))));
21748                };
21749                out.push_str(&alloc::format!("{d:04b}"));
21750            }
21751            out
21752        } else {
21753            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21754                return Some(Err(self.err(alloc::format!(
21755                    "invalid binary digit {bad:?} in B'…' bit string"
21756                ))));
21757            }
21758            body
21759        };
21760        // Route through the postfix-cast loop so a chained cast like
21761        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21762        // of erroring at the `::`.
21763        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21764        // literal keeps its exact length, while an explicit `::bit` cast is
21765        // bit(1) with pad/truncate semantics (PG).
21766        Some(self.finish_postfix_casts(Expr::Cast {
21767            expr: Box::new(Expr::Literal(Literal::String(bits))),
21768            target: CastTarget::Named("__bit_literal".to_string()),
21769        }))
21770    }
21771
21772    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21773        if let Some(res) = self.try_parse_bit_string_literal() {
21774            return res;
21775        }
21776        let tok_pos = self.pos;
21777        match self.advance() {
21778            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21779            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21780            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21781            // carrying the source mantissa + scale so no precision is lost. A
21782            // literal too wide for i128 falls back to double precision.
21783            // Out-of-line (#[inline(never)]) — this arm sits on the
21784            // parse_expr recursion chain; its expansion locals must not
21785            // widen the recursive frame (debug frame-cliff discipline).
21786            Token::Numeric(s) => match numeric_token_to_literal(s) {
21787                Ok(lit) => Ok(Expr::Literal(lit)),
21788                Err(msg) => Err(self.err(msg)),
21789            },
21790            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21791            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21792            // (the lexer only emits this token in the MySQL dialect). Lower
21793            // onto the existing bytea cast; out-of-line to keep this arm off
21794            // the parse recursion frame.
21795            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21796            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21797            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21798            Token::Null => Ok(Expr::Literal(Literal::Null)),
21799            // v6.1.1 — `$N` placeholder. The actual Value lookup
21800            // happens in the engine eval path against the prepared-
21801            // statement bind buffer.
21802            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21803            Token::LParen => {
21804                // v4.10: `(SELECT ...)` in expression position is a
21805                // scalar subquery; otherwise it's a parenthesised
21806                // expression. Peek for SELECT keyword to dispatch.
21807                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21808                // lexes as Ident("with") (not a reserved token). The subquery body
21809                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21810                // so its large `Statement` local stays out of parse_atom's stack
21811                // frame — parse_atom is on the recursive `((…))` cycle and the
21812                // nesting budget is tuned to its frame size).
21813                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21814                    if s.eq_ignore_ascii_case("with"));
21815                if matches!(self.peek(), Token::Select) || is_with {
21816                    self.parse_paren_scalar_subquery(is_with)
21817                } else {
21818                    let e = self.parse_expr(0)?;
21819                    // `(a, b, …)` — a row constructor. Valid only
21820                    // in front of a comparison operator or [NOT]
21821                    // IN; both expand at parse time (lexicographic
21822                    // comparison / OR'd row equalities).
21823                    if matches!(self.peek(), Token::Comma) {
21824                        let mut row = alloc::vec![e];
21825                        while matches!(self.peek(), Token::Comma) {
21826                            self.advance();
21827                            row.push(self.parse_expr(0)?);
21828                        }
21829                        if !matches!(self.peek(), Token::RParen) {
21830                            return Err(self.err(alloc::format!(
21831                                "expected ')' after row constructor, got {:?}",
21832                                self.peek()
21833                            )));
21834                        }
21835                        self.advance();
21836                        // A bare `(a, b, …)` row constructor can carry postfix
21837                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21838                        // early return here skips parse_atom's tail postfix
21839                        // pass, so fold casts in explicitly. For the
21840                        // comparison / predicate forms nothing postfix follows,
21841                        // so this is a no-op.
21842                        return self
21843                            .parse_row_comparison_tail(row)
21844                            .and_then(|e| self.finish_postfix_casts(e));
21845                    }
21846                    match self.advance() {
21847                        Token::RParen => Ok(e),
21848                        other => Err(ParseError {
21849                            message: format!("expected ')', got {other:?}"),
21850                            token_pos: self.consumed_pos(),
21851                        }),
21852                    }
21853                }
21854            }
21855            Token::LBracket => self.parse_vector_literal_body(),
21856            Token::Extract => self.parse_extract_atom(),
21857            Token::Interval => self.parse_interval_atom(),
21858            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21859            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21860            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21861            // expression position calling the PG `left(string, n)` /
21862            // `right(string, n)` function; rebuild the AST as a regular
21863            // function call so the engine's apply_function dispatch picks
21864            // it up. Delegated to a #[inline(never)] helper so its locals
21865            // don't bloat this recursive `parse_atom` frame (the nesting
21866            // budget in `enter_nested` is tuned to parse_atom's size).
21867            Token::Left if matches!(self.peek(), Token::LParen) => {
21868                self.parse_lr_string_function_call("left")
21869            }
21870            Token::Right if matches!(self.peek(), Token::LParen) => {
21871                self.parse_lr_string_function_call("right")
21872            }
21873            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21874            // token; we match on the bare ident. NOT is a token
21875            // (consumed in the comparison rung), but `EXISTS (...)`
21876            // at the top of an expression starts here.
21877            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21878                self.parse_exists_atom(false)
21879            }
21880            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21881            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21882            // CASE is a bare ident; we dispatch on lowercase match.
21883            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21884                self.parse_case_atom()
21885            }
21886            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21887            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21888            // '…'`. Lower onto the ::cast node so the existing
21889            // runtime text→date/timestamp paths do the parsing. The
21890            // string must follow immediately, else the ident stays a
21891            // plain column reference.
21892            Token::Ident(s)
21893                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21894                    && matches!(self.peek(), Token::String(_)) =>
21895            {
21896                let target =
21897                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21898                let Token::String(lit) = self.advance() else {
21899                    unreachable!("peek guaranteed a string token");
21900                };
21901                Ok(Expr::Cast {
21902                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21903                    target,
21904                })
21905            }
21906            // v7.39 (round 221) — the SQL-standard long spellings:
21907            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21908            // TIME ZONE '…'`. Consume the modifier and lower to the same
21909            // typed-literal cast (`timetz` / `timestamptz` for WITH).
21910            Token::Ident(s)
21911                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21912                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21913                        || w.eq_ignore_ascii_case("without"))
21914                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21915                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21916                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21917            {
21918                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21919                self.advance(); // WITH / WITHOUT
21920                self.advance(); // TIME
21921                self.advance(); // ZONE
21922                let Token::String(lit) = self.advance() else {
21923                    unreachable!("guard checked a string token");
21924                };
21925                let base = s.to_ascii_lowercase();
21926                let target = match (base.as_str(), with_tz) {
21927                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21928                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21929                    (_, true) => CastTarget::Timestamptz,
21930                    (_, false) => CastTarget::Timestamp,
21931                };
21932                Ok(Expr::Cast {
21933                    expr: Box::new(Expr::Literal(Literal::String(lit))),
21934                    target,
21935                })
21936            }
21937            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21938            // gathers the subquery's single-column rows (in its row order)
21939            // into an array. Desugared to `array_agg` over the subquery as a
21940            // derived table; out-of-line to keep parse_atom's frame small (it
21941            // sits on the recursive nesting-budget cycle).
21942            Token::Ident(s) | Token::QuotedIdent(s)
21943                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21944            {
21945                self.parse_array_subquery()
21946            }
21947            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21948            // is not a reserved token; we match by case-insensitive
21949            // ident. The opening `[` must follow immediately. v7.39 (read01
21950            // round 105) — the body moved out-of-line so its `Vec`/loop locals
21951            // leave parse_atom's frame (which sits on the nesting-budget cycle).
21952            Token::Ident(s) | Token::QuotedIdent(s)
21953                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21954            {
21955                self.parse_array_literal_body()
21956            }
21957            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21958            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21959            // We special-case before the generic ident dispatch so
21960            // the AGAINST clause never reaches the function-call
21961            // loop (which would mis-read `(cols) AGAINST` as a
21962            // call with no trailing modifier). The shape is
21963            // rewritten to a Boolean OR over per-column
21964            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21965            // term)` so the existing FTS evaluator handles
21966            // semantics — the fulltext-GIN built at CREATE TABLE
21967            // time is currently a "real index that survives dump
21968            // round-trip"; the planner hook that actually uses
21969            // it for posting-list intersection lands in a later
21970            // sub-phase (Phase 2.2b) without touching this surface.
21971            Token::Ident(s) | Token::QuotedIdent(s)
21972                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21973            {
21974                self.parse_match_against_atom()
21975            }
21976            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21977            // v7.37.43-T4 — PG-unreserved keywords are legal column /
21978            // alias names in expression context too. `release` appears
21979            // in sentori `0003_partition_events.sql` as both a column
21980            // reference (SELECT … release …) and an INSERT column list
21981            // entry. Mirrors `expect_ident_like`'s expansion of the
21982            // identifier set.
21983            other if unreserved_keyword_text(&other).is_some() => {
21984                let s = unreserved_keyword_text(&other).unwrap();
21985                self.finish_ident_atom(s)
21986            }
21987            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21988            // only inside `SET` before, so `SELECT @@autocommit` — which
21989            // every MySQL connector asks at handshake — was a parse error.
21990            // MariaDB accepts the bare, `@@session.` and `@@global.`
21991            // spellings alike and answers from the session's own value.
21992            Token::SessionVar(v) => {
21993                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21994                // has nothing to do with a `@@` engine setting: its own
21995                // per-session namespace, and an unset one reads NULL instead
21996                // of raising. Stripping every `@` (as this did) made `@x` and
21997                // `@@x` the same node, so `SELECT @x` answered "Unknown
21998                // system variable".
21999                Ok(variable_ref_atom(&v))
22000            }
22001            other => Err(ParseError {
22002                message: format!("unexpected token {other:?} in expression"),
22003                token_pos: tok_pos,
22004            }),
22005        }
22006        // After parsing the atom, fold any postfix `::vector` casts.
22007        .and_then(|atom| self.finish_postfix_casts(atom))
22008    }
22009
22010    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22011    /// Both bind tighter than any binary op.
22012    /// Shared cast-target parser for postfix `::TYPE` and the
22013    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22014    /// If the next tokens are `( N )`, consume them and return the canonical
22015    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22016    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22017    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22018        if !matches!(self.peek(), Token::LParen) {
22019            return None;
22020        }
22021        self.advance(); // (
22022        let n = match self.advance() {
22023            Token::Integer(n) => n,
22024            _ => return Some(base.to_string()), // malformed → drop precision
22025        };
22026        if matches!(self.peek(), Token::RParen) {
22027            self.advance();
22028        }
22029        Some(alloc::format!("{base}({n})"))
22030    }
22031
22032    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22033        let target = match self.advance() {
22034            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22035                "int" | "integer" | "int4" => {
22036                    if matches!(self.peek(), Token::LBracket)
22037                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22038                    {
22039                        self.advance();
22040                        self.advance();
22041                        CastTarget::IntArray
22042                    } else {
22043                        CastTarget::Int
22044                    }
22045                }
22046                "bigint" | "int8" => {
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::BigIntArray
22053                    } else {
22054                        CastTarget::BigInt
22055                    }
22056                }
22057                "float" | "double" => CastTarget::Float,
22058                "text" => {
22059                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22060                    if matches!(self.peek(), Token::LBracket)
22061                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22062                    {
22063                        self.advance();
22064                        self.advance();
22065                        CastTarget::TextArray
22066                    } else {
22067                        CastTarget::Text
22068                    }
22069                }
22070                "bool" | "boolean" => CastTarget::Bool,
22071                "vector" => CastTarget::Vector,
22072                "date" => CastTarget::Date,
22073                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22074                // seconds precision through the Named path (the engine rounds
22075                // the sub-second field); bare `::timestamp` keeps the fast arm.
22076                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22077                    Some(named) => CastTarget::Named(named),
22078                    None => CastTarget::Timestamp,
22079                },
22080                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22081                    Some(named) => CastTarget::Named(named),
22082                    None => CastTarget::Timestamptz,
22083                },
22084                "interval" => CastTarget::Interval,
22085                "json" => CastTarget::Json,
22086                "jsonb" => CastTarget::Jsonb,
22087                // v7.39 (round 694) — these have dedicated CastTarget
22088                // variants, so they never reached the postfix `[]` handling
22089                // further down and `::regtype[]` was a SYNTAX error at the
22090                // `]`. PG has an array type for every scalar; take the
22091                // suffix here and hand the canonical `<ty>_array` name to
22092                // the engine, the same shape every other array cast uses.
22093                "regtype" if self.peek_postfix_array_brackets() => {
22094                    self.advance();
22095                    self.advance();
22096                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22097                }
22098                "regclass" if self.peek_postfix_array_brackets() => {
22099                    self.advance();
22100                    self.advance();
22101                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22102                }
22103                "regtype" => CastTarget::RegType,
22104                "regclass" => CastTarget::RegClass,
22105                // v7.12.0 — `::tsvector` / `::tsquery`.
22106                // Engine decodes the LHS text via the PG
22107                // external form parser.
22108                // v7.39 (round 352, M8) — MySQL's own cast targets.
22109                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22110                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22111                // such type, so they are taken only in that dialect and
22112                // fall through to the "type does not exist" arm otherwise.
22113                "signed" | "unsigned" if self.mysql_dialect => {
22114                    if matches!(self.peek(), Token::Ident(k)
22115                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22116                    {
22117                        self.advance();
22118                    }
22119                    CastTarget::Named(s.to_ascii_lowercase())
22120                }
22121                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22122                // in MySQL: MariaDB answers '123' where the SQL-standard
22123                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22124                // Truncating a number to its first digit is a wrong answer
22125                // with no error, so the MySQL session gets MySQL's reading.
22126                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22127                    CastTarget::Text
22128                }
22129                "tsvector" => CastTarget::TsVector,
22130                "tsquery" => CastTarget::TsQuery,
22131                // v7.17.0 — `::uuid`. Engine decodes the LHS
22132                // text via `spg_storage::parse_uuid_str`.
22133                "uuid" => CastTarget::Uuid,
22134                // v7.18 — `::bytea`. Engine decodes the LHS
22135                // text via the PG hex form (`'\xdeadbeef'`)
22136                // or escape form (`'\\x05\\x00'`). Closes
22137                // mailrs D-pre #3 reverse-acceptance gap.
22138                "bytea" => CastTarget::Bytea,
22139                // v7.37.5 ship triage — generic typed-cast escape.
22140                // Anything the long-tail PG type ident table knows
22141                // about(network/bit/geometry/multirange/etc.)flows
22142                // through `CastTarget::Named(canonical)`; the engine
22143                // resolves via `column_type_to_data_type` and dispatches
22144                // through the typed `coerce_value` path. Truly
22145                // unrecognised idents still hit the error arm below
22146                // because the engine rejects them.
22147                other => {
22148                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22149                    // `::varchar(255)`, etc. Capture into the canonical
22150                    // `name(p,s)` form so `type_name_to_data_type` can
22151                    // reconstruct the `DataType::Numeric { precision,
22152                    // scale }` (and similar param-carrying types).
22153                    let mut name = other.to_string();
22154                    // v7.39 (round 281) — `::bit varying(3)` is two
22155                    // words; fold the tail in so the typmod reaches the
22156                    // type resolver instead of tripping the parser.
22157                    if name.eq_ignore_ascii_case("bit")
22158                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22159                    {
22160                        self.advance();
22161                        name = alloc::string::String::from("varbit");
22162                    }
22163                    // v7.39 (round 613) — `::character varying` is the same
22164                    // two-word shape and had no fold, so the `varying` was
22165                    // left behind and the cast became a bare `character`,
22166                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22167                    // `a` where PG answers `ab`. Silently, and for a spelling
22168                    // pg_dump writes.
22169                    if name.eq_ignore_ascii_case("character")
22170                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22171                    {
22172                        self.advance();
22173                        name = alloc::string::String::from("varchar");
22174                    }
22175                    if matches!(self.peek(), Token::LParen) {
22176                        let mut buf = alloc::string::String::from("(");
22177                        let mut depth = 0usize;
22178                        loop {
22179                            match self.advance() {
22180                                Token::LParen => {
22181                                    depth += 1;
22182                                    if depth > 1 {
22183                                        buf.push('(');
22184                                    }
22185                                }
22186                                Token::RParen => {
22187                                    depth -= 1;
22188                                    if depth == 0 {
22189                                        buf.push(')');
22190                                        break;
22191                                    }
22192                                    buf.push(')');
22193                                }
22194                                Token::Comma => buf.push(','),
22195                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22196                                // v7.39 (round 273) — a minus used to fall
22197                                // into the catch-all below and vanish, so
22198                                // `::numeric(10,-2)` reached the engine as
22199                                // the text `numeric(10,2)` and silently
22200                                // rounded to two DECIMALS instead of to
22201                                // hundreds. A dropped token is not a
22202                                // no-op when it carries a sign.
22203                                Token::Minus => buf.push('-'),
22204                                Token::Eof => break,
22205                                _ => {}
22206                            }
22207                        }
22208                        name.push_str(&buf);
22209                    }
22210                    // Optional postfix `[]` widens to the array form —
22211                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22212                    // The engine's `type_name_to_data_type` recognises
22213                    // the canonical `<ty>_array` form.
22214                    if matches!(self.peek(), Token::LBracket)
22215                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22216                    {
22217                        self.advance();
22218                        self.advance();
22219                        name.push_str("_array");
22220                    }
22221                    CastTarget::Named(name)
22222                }
22223            },
22224            Token::Interval => CastTarget::Interval,
22225            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22226            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22227            // = char(1)); other quoted names resolve like idents.
22228            Token::QuotedIdent(q) => {
22229                if q.eq_ignore_ascii_case("char") {
22230                    CastTarget::Named("char1".into())
22231                } else {
22232                    CastTarget::Named(q.to_ascii_lowercase())
22233                }
22234            }
22235            other => {
22236                return Err(ParseError {
22237                    message: format!("expected type ident after `::`, got {other:?}"),
22238                    token_pos: self.consumed_pos(),
22239                });
22240            }
22241        };
22242        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22243        // target to its array sibling. Closed-enum arms (Bool /
22244        // SmallInt / Numeric / Float / Date / …) didn't carry the
22245        // explicit widening that Text / Int / BigInt did, so
22246        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22247        // error. The widening here mirrors the per-arm Text /
22248        // Int / BigInt logic above + folds the new ζ-A first-class
22249        // types through `CastTarget::Named("<ty>_array")`.
22250        if matches!(self.peek(), Token::LBracket)
22251            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22252        {
22253            let widened = match &target {
22254                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22255                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22256                // v7.39 (round 326, V43) — the two temporal types stay
22257                // distinct. Both used to widen to `timestamptz_array`, so
22258                // `::timestamp[]` named the wrong target in its own error
22259                // message and lost the zone-less identity on the way.
22260                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22261                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22262                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22263                CastTarget::Json | CastTarget::Jsonb => {
22264                    Some(CastTarget::Named("jsonb_array".to_string()))
22265                }
22266                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22267                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22268                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22269                CastTarget::Named(name) => {
22270                    let mut a = name.clone();
22271                    a.push_str("_array");
22272                    Some(CastTarget::Named(a))
22273                }
22274                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22275                // RegType / RegClass / TextArray / IntArray /
22276                // BigIntArray already finalised — leave as is.
22277                _ => None,
22278            };
22279            if let Some(w) = widened {
22280                self.advance();
22281                self.advance();
22282                return Ok(w);
22283            }
22284        }
22285        Ok(target)
22286    }
22287
22288    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22289        loop {
22290            // v7.38 (read01, T9) — composite field access `(expr).field`.
22291            // A bare `a.b` is consumed as a qualified column inside the ident
22292            // atom, so a Dot only survives to this postfix position when the
22293            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22294            // `.*` whole-row expansion is not handled here (projection-level).
22295            if matches!(self.peek(), Token::Dot)
22296                && matches!(
22297                    self.tokens.get(self.pos + 1),
22298                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22299                )
22300            {
22301                self.advance(); // .
22302                let field = match self.advance() {
22303                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22304                    other => {
22305                        return Err(
22306                            self.err(format!("expected a field name after '.', got {other:?}"))
22307                        );
22308                    }
22309                };
22310                expr = Expr::FieldAccess {
22311                    base: Box::new(expr),
22312                    field,
22313                };
22314                continue;
22315            }
22316            if matches!(self.peek(), Token::DoubleColon) {
22317                self.advance();
22318                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22319                // target set to include INTERVAL (reserved Token),
22320                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22321                // mailrs follow-up H3a + H3b.
22322                let target = self.parse_cast_target()?;
22323                expr = Expr::Cast {
22324                    expr: Box::new(expr),
22325                    target,
22326                };
22327                continue;
22328            }
22329            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22330            // returns NULL for out-of-range. Multiple subscripts
22331            // chain: `a[i][j]` parses left-to-right.
22332            if matches!(self.peek(), Token::LBracket) {
22333                self.advance();
22334                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22335                // bare index stays a subscript.
22336                let lo = if matches!(self.peek(), Token::Colon) {
22337                    None
22338                } else {
22339                    Some(self.parse_expr(0)?)
22340                };
22341                if matches!(self.peek(), Token::Colon) {
22342                    self.advance();
22343                    let hi = if matches!(self.peek(), Token::RBracket) {
22344                        None
22345                    } else {
22346                        Some(Box::new(self.parse_expr(0)?))
22347                    };
22348                    if !matches!(self.peek(), Token::RBracket) {
22349                        return Err(self.err(alloc::format!(
22350                            "expected ']' after array slice, got {:?}",
22351                            self.peek()
22352                        )));
22353                    }
22354                    self.advance();
22355                    expr = Expr::ArraySlice {
22356                        target: Box::new(expr),
22357                        lo: lo.map(Box::new),
22358                        hi,
22359                    };
22360                    continue;
22361                }
22362                let index = lo.expect("non-colon branch parsed an index");
22363                if !matches!(self.peek(), Token::RBracket) {
22364                    return Err(self.err(alloc::format!(
22365                        "expected ']' after array index, got {:?}",
22366                        self.peek()
22367                    )));
22368                }
22369                self.advance();
22370                expr = Expr::ArraySubscript {
22371                    target: Box::new(expr),
22372                    index: Box::new(index),
22373                };
22374                continue;
22375            }
22376            // `expr AT TIME ZONE zone` — lowers to PG's own function
22377            // form timezone(zone, expr); the scalar implements the
22378            // offset shift (named zones error there — no tzdata).
22379            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22380                && matches!(self.tokens.get(self.pos + 1),
22381                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22382                && matches!(self.tokens.get(self.pos + 2),
22383                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22384            {
22385                self.advance(); // AT
22386                self.advance(); // TIME
22387                self.advance(); // ZONE
22388                // Zone at comparison precedence so AND/OR stay out.
22389                let zone = self.parse_expr(6)?;
22390                expr = Expr::FunctionCall {
22391                    name: "timezone".to_string(),
22392                    args: alloc::vec![zone, expr],
22393                };
22394                continue;
22395            }
22396            // `expr COLLATE "name"` — SPG's single text ordering IS
22397            // byte order, i.e. the C collation. The byte-order
22398            // spellings absorb as no-ops; a locale collation would
22399            // silently sort differently from PG, so it errors
22400            // honestly instead.
22401            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22402                self.advance();
22403                let mut cname = match self.advance() {
22404                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22405                    other => {
22406                        return Err(self.err(alloc::format!(
22407                            "expected collation name after COLLATE, got {other:?}"
22408                        )));
22409                    }
22410                };
22411                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22412                // is how `pg_dump` writes the default one:
22413                // `… COLLATE pg_catalog.default`. Reading a single token
22414                // left the SCHEMA as the name, so the clause was refused
22415                // as an unsupported locale collation and no dump ran.
22416                if matches!(self.peek(), Token::Dot) {
22417                    self.advance();
22418                    cname = match self.advance() {
22419                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22420                        // `default` lexes as a KEYWORD, and it is the name
22421                        // pg_dump writes — the same trap round 535 hit with
22422                        // TABLE / INDEX / FULL.
22423                        Token::Default => alloc::string::String::from("default"),
22424                        other => {
22425                            return Err(self.err(alloc::format!(
22426                                "expected collation name after COLLATE, got {other:?}"
22427                            )));
22428                        }
22429                    };
22430                }
22431                let lc = cname.to_ascii_lowercase();
22432                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22433                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22434                // family / `binary`) forces byte-wise, which is exactly
22435                // what `BINARY expr` does — lower onto that so every fold
22436                // site (comparison, LIKE, ORDER BY) suppresses via
22437                // `is_binary_coerced`. A `_ci` family override folds, and
22438                // under the MySQL dialect the default already folds, so it
22439                // absorbs as a no-op; likewise the C / byte-order spellings.
22440                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22441                    expr = Expr::Cast {
22442                        expr: alloc::boxed::Box::new(expr),
22443                        target: CastTarget::Named("binary".to_string()),
22444                    };
22445                    continue;
22446                }
22447                let mysql_ci = self.mysql_dialect
22448                    && (lc.ends_with("_ci")
22449                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22450                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22451                // goes to the lowering channel, the byte-order spellings
22452                // included. Round 691 recorded only the names the old
22453                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22454                // absorbed as a no-op — and once a column could declare a
22455                // collation, absorbing the clause meant the COLUMN's
22456                // collation won where the query had asked for bytes.
22457                if self.in_order_by_key && !mysql_ci {
22458                    self.order_key_collation = Some(cname);
22459                    continue;
22460                }
22461                if !matches!(
22462                    lc.as_str(),
22463                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22464                ) && !mysql_ci
22465                {
22466                    return Err(self.err(alloc::format!(
22467                        "COLLATE {cname:?}: SPG orders text by bytes (the C \
22468                         collation); locale collations are not supported yet — \
22469                         use COLLATE \"C\" or drop the clause"
22470                    )));
22471                }
22472                continue;
22473            }
22474            return Ok(expr);
22475        }
22476    }
22477
22478    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22479    /// the first token that is not one. Schema qualifiers collapse to the
22480    /// last part, which is what every other name path here does (SPG is
22481    /// single-schema).
22482    fn take_comma_separated_names(&mut self) -> Vec<String> {
22483        let mut out = Vec::new();
22484        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22485            self.advance();
22486            let mut last = n;
22487            while matches!(self.peek(), Token::Dot) {
22488                self.advance();
22489                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22490                    last = t;
22491                }
22492            }
22493            out.push(last);
22494            if matches!(self.peek(), Token::Comma) {
22495                self.advance();
22496            } else {
22497                break;
22498            }
22499        }
22500        out
22501    }
22502
22503    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22504    ///
22505    /// The general cast-target path tests this inline; the types with their
22506    /// own `CastTarget` variant need it as a guard on their match arm,
22507    /// which is what this exists for.
22508    fn peek_postfix_array_brackets(&self) -> bool {
22509        matches!(self.peek(), Token::LBracket)
22510            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22511    }
22512
22513    /// Parse the operator tail after a `(a, b, …)` row constructor
22514    /// and expand at parse time. `=` is the conjunction of element
22515    /// equalities; `<>` its negation; the order operators expand
22516    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22517    /// equalities. Anything else (a bare row value, a subquery
22518    /// RHS) errors honestly — SPG has no composite runtime value.
22519    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22520        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22521            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22522                lhs: Box::new(l.clone()),
22523                op: BinOp::Eq,
22524                rhs: Box::new(r.clone()),
22525            });
22526            let first = it.next().expect("row has at least two elements");
22527            it.fold(first, |acc, e| Expr::Binary {
22528                lhs: Box::new(acc),
22529                op: BinOp::And,
22530                rhs: Box::new(e),
22531            })
22532        }
22533        // Lexicographic (a,b) OP (c,d):
22534        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22535        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22536            if lhs.len() == 1 {
22537                return Expr::Binary {
22538                    lhs: Box::new(lhs[0].clone()),
22539                    op: last,
22540                    rhs: Box::new(rhs[0].clone()),
22541                };
22542            }
22543            let head_strict = Expr::Binary {
22544                lhs: Box::new(lhs[0].clone()),
22545                op: strict,
22546                rhs: Box::new(rhs[0].clone()),
22547            };
22548            let head_eq = Expr::Binary {
22549                lhs: Box::new(lhs[0].clone()),
22550                op: BinOp::Eq,
22551                rhs: Box::new(rhs[0].clone()),
22552            };
22553            Expr::Binary {
22554                lhs: Box::new(head_strict),
22555                op: BinOp::Or,
22556                rhs: Box::new(Expr::Binary {
22557                    lhs: Box::new(head_eq),
22558                    op: BinOp::And,
22559                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22560                }),
22561            }
22562        }
22563        let negated_in = if matches!(self.peek(), Token::Not)
22564            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22565        {
22566            self.advance();
22567            true
22568        } else {
22569            false
22570        };
22571        if matches!(self.peek(), Token::In) {
22572            self.advance();
22573            if !matches!(self.peek(), Token::LParen) {
22574                return Err(self.err(alloc::format!(
22575                    "expected '(' after row IN, got {:?}",
22576                    self.peek()
22577                )));
22578            }
22579            self.advance();
22580            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22581            // not a list of literal rows. Row-vs-list decomposes to
22582            // OR-of-AND above, but the subquery's rows are only known at
22583            // runtime, so keep it as a RowInSubquery node.
22584            if matches!(self.peek(), Token::Select) {
22585                let inner = self.parse_select_stmt()?;
22586                if !matches!(self.peek(), Token::RParen) {
22587                    return Err(self.err(alloc::format!(
22588                        "expected ')' after row IN-subquery, got {:?}",
22589                        self.peek()
22590                    )));
22591                }
22592                self.advance();
22593                let Statement::Select(s) = inner else {
22594                    unreachable!("parse_select_stmt always returns Statement::Select")
22595                };
22596                return Ok(Expr::RowInSubquery {
22597                    row,
22598                    subquery: Box::new(s),
22599                    negated: negated_in,
22600                });
22601            }
22602            let mut alternatives: Vec<Expr> = Vec::new();
22603            loop {
22604                // Optional ROW keyword before the paren row.
22605                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22606                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22607                {
22608                    self.advance();
22609                }
22610                if !matches!(self.peek(), Token::LParen) {
22611                    return Err(self.err(alloc::format!(
22612                        "expected '(' to open a row inside IN, got {:?}",
22613                        self.peek()
22614                    )));
22615                }
22616                self.advance();
22617                let mut rhs = alloc::vec![self.parse_expr(0)?];
22618                while matches!(self.peek(), Token::Comma) {
22619                    self.advance();
22620                    rhs.push(self.parse_expr(0)?);
22621                }
22622                if !matches!(self.peek(), Token::RParen) {
22623                    return Err(self.err(alloc::format!(
22624                        "expected ')' after row inside IN, got {:?}",
22625                        self.peek()
22626                    )));
22627                }
22628                self.advance();
22629                if rhs.len() != row.len() {
22630                    return Err(self.err(alloc::format!(
22631                        "row IN arity mismatch: left has {}, right has {}",
22632                        row.len(),
22633                        rhs.len()
22634                    )));
22635                }
22636                alternatives.push(row_eq(&row, &rhs));
22637                if matches!(self.peek(), Token::Comma) {
22638                    self.advance();
22639                    continue;
22640                }
22641                break;
22642            }
22643            if !matches!(self.peek(), Token::RParen) {
22644                return Err(self.err(alloc::format!(
22645                    "expected ')' to close row IN list, got {:?}",
22646                    self.peek()
22647                )));
22648            }
22649            self.advance();
22650            let mut it = alternatives.into_iter();
22651            let first = it.next().expect("IN list has at least one row");
22652            let combined = it.fold(first, |acc, e| Expr::Binary {
22653                lhs: Box::new(acc),
22654                op: BinOp::Or,
22655                rhs: Box::new(e),
22656            });
22657            return Ok(maybe_not(combined, negated_in));
22658        }
22659        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22660        // two periods share at least one time point. Each pair is
22661        // normalised with least/greatest (PG accepts the endpoints
22662        // in either order), then lowered to the standard
22663        // `start1 < end2 AND start2 < end1` form.
22664        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22665            if row.len() != 2 {
22666                return Err(self.err(alloc::format!(
22667                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22668                    row.len()
22669                )));
22670            }
22671            self.advance();
22672            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22673                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22674            {
22675                self.advance();
22676            }
22677            if !matches!(self.peek(), Token::LParen) {
22678                return Err(self.err(alloc::format!(
22679                    "expected '(' after OVERLAPS, got {:?}",
22680                    self.peek()
22681                )));
22682            }
22683            self.advance();
22684            let r0 = self.parse_expr(0)?;
22685            if !matches!(self.peek(), Token::Comma) {
22686                return Err(self.err(alloc::format!(
22687                    "OVERLAPS needs (start, end) on the right, got {:?}",
22688                    self.peek()
22689                )));
22690            }
22691            self.advance();
22692            let r1 = self.parse_expr(0)?;
22693            if !matches!(self.peek(), Token::RParen) {
22694                return Err(self.err(alloc::format!(
22695                    "expected ')' after OVERLAPS pair, got {:?}",
22696                    self.peek()
22697                )));
22698            }
22699            self.advance();
22700            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22701                name: String::from(name),
22702                args: alloc::vec![a.clone(), b.clone()],
22703            };
22704            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22705                lhs: Box::new(lhs),
22706                op: BinOp::Lt,
22707                rhs: Box::new(rhs),
22708            };
22709            return Ok(Expr::Binary {
22710                lhs: Box::new(lt(
22711                    pair_fn("least", &row[0], &row[1]),
22712                    pair_fn("greatest", &r0, &r1),
22713                )),
22714                op: BinOp::And,
22715                rhs: Box::new(lt(
22716                    pair_fn("least", &r0, &r1),
22717                    pair_fn("greatest", &row[0], &row[1]),
22718                )),
22719            });
22720        }
22721        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22722        // PG, `IS NULL` is true only when EVERY field is NULL, and
22723        // `IS NOT NULL` is true only when every field is non-NULL — the
22724        // latter is NOT the negation of the former (a mixed row is
22725        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22726        // which reproduces exactly that all-fields semantics.
22727        if matches!(self.peek(), Token::Is) {
22728            self.advance();
22729            let negated = if matches!(self.peek(), Token::Not) {
22730                self.advance();
22731                true
22732            } else {
22733                false
22734            };
22735            if !matches!(self.peek(), Token::Null) {
22736                return Err(self.err(alloc::format!(
22737                    "expected NULL after row IS [NOT], got {:?}",
22738                    self.peek()
22739                )));
22740            }
22741            self.advance();
22742            let mut it = row.iter().map(|e| Expr::IsNull {
22743                expr: Box::new(e.clone()),
22744                negated,
22745            });
22746            let first = it.next().expect("row has at least two elements");
22747            return Ok(it.fold(first, |acc, e| Expr::Binary {
22748                lhs: Box::new(acc),
22749                op: BinOp::And,
22750                rhs: Box::new(e),
22751            }));
22752        }
22753        let op = match self.peek() {
22754            Token::Eq => BinOp::Eq,
22755            Token::NotEq => BinOp::NotEq,
22756            Token::Lt => BinOp::Lt,
22757            Token::LtEq => BinOp::LtEq,
22758            Token::Gt => BinOp::Gt,
22759            Token::GtEq => BinOp::GtEq,
22760            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22761            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22762            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22763            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22764            // (`::text`, `.field`) applies at the caller just as it does for the
22765            // ROW(...) node. All the comparison / predicate forms returned above.
22766            _ => {
22767                return Ok(Expr::FunctionCall {
22768                    name: String::from("row"),
22769                    args: row,
22770                });
22771            }
22772        };
22773        self.advance();
22774        // Optional ROW keyword before the paren row.
22775        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22776            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22777        {
22778            self.advance();
22779        }
22780        if !matches!(self.peek(), Token::LParen) {
22781            return Err(self.err(alloc::format!(
22782                "expected '(' to open the right-hand row, got {:?}",
22783                self.peek()
22784            )));
22785        }
22786        self.advance();
22787        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22788        // subquery. Kept as a node (the subquery's row is a runtime value);
22789        // the literal-RHS form below still decomposes at parse time.
22790        if matches!(self.peek(), Token::Select) {
22791            let inner = self.parse_select_stmt()?;
22792            if !matches!(self.peek(), Token::RParen) {
22793                return Err(self.err(alloc::format!(
22794                    "expected ')' after row comparison subquery, got {:?}",
22795                    self.peek()
22796                )));
22797            }
22798            self.advance();
22799            let Statement::Select(s) = inner else {
22800                unreachable!("parse_select_stmt always returns Statement::Select")
22801            };
22802            return Ok(Expr::RowCmpSubquery {
22803                row,
22804                op,
22805                subquery: Box::new(s),
22806            });
22807        }
22808        let mut rhs = alloc::vec![self.parse_expr(0)?];
22809        while matches!(self.peek(), Token::Comma) {
22810            self.advance();
22811            rhs.push(self.parse_expr(0)?);
22812        }
22813        if !matches!(self.peek(), Token::RParen) {
22814            return Err(self.err(alloc::format!(
22815                "expected ')' after right-hand row, got {:?}",
22816                self.peek()
22817            )));
22818        }
22819        self.advance();
22820        if rhs.len() != row.len() {
22821            // v7.39 (round 239) — PG's wording (42601).
22822            return Err(self.err("unequal number of entries in row expressions".to_string()));
22823        }
22824        Ok(match op {
22825            BinOp::Eq => row_eq(&row, &rhs),
22826            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22827            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22828            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22829            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22830            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22831            _ => unreachable!("op restricted above"),
22832        })
22833    }
22834
22835    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22836    /// escape character becomes the matcher's default backslash:
22837    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22838    /// → the char itself, and any pre-existing backslash escapes
22839    /// itself so it stays literal. Both operands must be string
22840    /// literals — a runtime pattern would need matcher support.
22841    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22842        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22843            (&pattern, &esc)
22844        else {
22845            return Err(
22846                "LIKE ... ESCAPE requires string-literal pattern and escape \
22847                 (runtime escape characters are not supported yet)"
22848                    .into(),
22849            );
22850        };
22851        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22852        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22853        // multi-character escape is an error.
22854        let esc_ch: Option<char> = {
22855            let mut ch_iter = e.chars();
22856            match (ch_iter.next(), ch_iter.next()) {
22857                (Some(c), None) => Some(c),
22858                (None, _) => None,
22859                (Some(_), Some(_)) => {
22860                    return Err(alloc::format!(
22861                        "ESCAPE must be a single character, got {e:?}"
22862                    ));
22863                }
22864            }
22865        };
22866        let mut out = String::with_capacity(p.len() + 4);
22867        let mut chars = p.chars();
22868        while let Some(c) = chars.next() {
22869            if Some(c) == esc_ch {
22870                match chars.next() {
22871                    // Escaped wildcard / escaped escape → keep the
22872                    // next char literal via backslash.
22873                    Some(next) => {
22874                        out.push('\\');
22875                        out.push(next);
22876                    }
22877                    None => {
22878                        return Err("LIKE pattern ends with the escape character".into());
22879                    }
22880                }
22881            } else if c == '\\' && esc_ch != Some('\\') {
22882                // A raw backslash is literal under a custom (or absent) escape
22883                // — escape it for the backslash-based matcher.
22884                out.push('\\');
22885                out.push('\\');
22886            } else {
22887                out.push(c);
22888            }
22889        }
22890        Ok(Expr::Literal(Literal::String(out)))
22891    }
22892
22893    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22894    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22895    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22896    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22897    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22898    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22899    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22900    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22901    /// array expression errors honestly rather than silently mismatching.
22902    fn try_like_any_all(
22903        &mut self,
22904        base: &Expr,
22905        negated: bool,
22906        case_insensitive: bool,
22907    ) -> Result<Option<Expr>, ParseError> {
22908        let is_any = match self.peek() {
22909            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22910            Token::Ident(s)
22911                if s.eq_ignore_ascii_case("any")
22912                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22913            {
22914                true
22915            }
22916            _ => return Ok(None),
22917        };
22918        self.advance(); // ANY / ALL
22919        self.advance(); // '('
22920        let arr = self.parse_expr(0)?;
22921        if !matches!(self.peek(), Token::RParen) {
22922            return Err(self.err(format!(
22923                "expected ')' after LIKE {} argument, got {:?}",
22924                if is_any { "ANY" } else { "ALL" },
22925                self.peek()
22926            )));
22927        }
22928        self.advance(); // ')'
22929        let Expr::Array(items) = arr else {
22930            return Err(self.err(
22931                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22932            ));
22933        };
22934        let mut clauses = items.into_iter().map(|p| Expr::Like {
22935            expr: Box::new(base.clone()),
22936            pattern: Box::new(p),
22937            negated,
22938            case_insensitive,
22939        });
22940        let Some(first) = clauses.next() else {
22941            // ANY(empty) = FALSE, ALL(empty) = TRUE.
22942            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22943        };
22944        let op = if is_any { BinOp::Or } else { BinOp::And };
22945        let combined = clauses.fold(first, |acc, c| Expr::Binary {
22946            lhs: Box::new(acc),
22947            op,
22948            rhs: Box::new(c),
22949        });
22950        Ok(Some(combined))
22951    }
22952
22953    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
22954    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22955    /// `AND` is not swallowed.
22956    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22957        self.advance(); // BETWEEN
22958        // SYMMETRIC — the bounds may arrive in either order; both
22959        // orientations OR together. ASYMMETRIC is the default and
22960        // absorbs as noise.
22961        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22962        {
22963            self.advance();
22964            true
22965        } else {
22966            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22967                self.advance();
22968            }
22969            false
22970        };
22971        let low = self.parse_expr(6)?;
22972        if !matches!(self.peek(), Token::And) {
22973            return Err(self.err(format!(
22974                "expected AND after BETWEEN low bound, got {:?}",
22975                self.peek()
22976            )));
22977        }
22978        self.advance();
22979        let high = self.parse_expr(6)?;
22980        let target = Box::new(expr);
22981        let range = |lo: Expr, hi: Expr| Expr::Binary {
22982            lhs: Box::new(Expr::Binary {
22983                lhs: target.clone(),
22984                op: BinOp::GtEq,
22985                rhs: Box::new(lo),
22986            }),
22987            op: BinOp::And,
22988            rhs: Box::new(Expr::Binary {
22989                lhs: target.clone(),
22990                op: BinOp::LtEq,
22991                rhs: Box::new(hi),
22992            }),
22993        };
22994        let combined = if symmetric {
22995            Expr::Binary {
22996                lhs: Box::new(range(low.clone(), high.clone())),
22997                op: BinOp::Or,
22998                rhs: Box::new(range(high, low)),
22999            }
23000        } else {
23001            range(low, high)
23002        };
23003        Ok(maybe_not(combined, negated))
23004    }
23005
23006    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23007    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23008    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23009    /// Caller already consumed the leading `WITH` ident.
23010    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23011    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23012    /// self-reference that appears more than once in a single term.
23013    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23014        use crate::ast::{CteBody, SelectStatement};
23015        if !cte.recursive {
23016            return Ok(());
23017        }
23018        let CteBody::Select(body) = &cte.body else {
23019            return Ok(());
23020        };
23021        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23022        // check the anchor and every peer term.
23023        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23024        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23025        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23026            return Err(self.err(String::from(
23027                "ORDER BY in a recursive query is not implemented",
23028            )));
23029        }
23030        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23031            return Err(self.err(String::from(
23032                "LIMIT in a recursive query is not implemented",
23033            )));
23034        }
23035        let self_refs = |s: &SelectStatement| -> usize {
23036            let Some(from) = &s.from else {
23037                return 0;
23038            };
23039            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23040            for j in &from.joins {
23041                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23042                    n += 1;
23043                }
23044            }
23045            n
23046        };
23047        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23048            return Err(self.err(alloc::format!(
23049                "recursive reference to query \"{}\" must not appear more than once",
23050                cte.name
23051            )));
23052        }
23053        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23054        // apply only when the body actually references itself (a non-self-
23055        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23056        let anchor_refs = self_refs(body);
23057        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23058        if anchor_refs > 0 || union_refs {
23059            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23060            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23061            // "does not have the form" error — SPG used to compute a value.
23062            if body.unions.is_empty()
23063                || body.unions.iter().any(|(k, _)| {
23064                    !matches!(
23065                        k,
23066                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23067                    )
23068                })
23069            {
23070                return Err(self.err(alloc::format!(
23071                    "recursive query \"{}\" does not have the form non-recursive-term \
23072                     UNION [ALL] recursive-term",
23073                    cte.name
23074                )));
23075            }
23076            if anchor_refs > 0 {
23077                return Err(self.err(alloc::format!(
23078                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23079                    cte.name
23080                )));
23081            }
23082        }
23083        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23084        for (_, u) in &body.unions {
23085            if self_refs(u) == 0 {
23086                continue;
23087            }
23088            // The self-reference must not sit on the nullable side of an outer
23089            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23090            if let Some(from) = &u.from {
23091                for (i, j) in from.joins.iter().enumerate() {
23092                    let left_has_self = is_self(&from.primary)
23093                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23094                    let violated = match j.kind {
23095                        crate::ast::JoinKind::Left => is_self(&j.table),
23096                        crate::ast::JoinKind::Right => left_has_self,
23097                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23098                        _ => false,
23099                    };
23100                    if violated {
23101                        return Err(self.err(alloc::format!(
23102                            "recursive reference to query \"{}\" must not appear within an outer join",
23103                            cte.name
23104                        )));
23105                    }
23106                }
23107            }
23108            // No aggregates at the top level of the recursive term (SPG used
23109            // to run them and surface a misleading downstream error).
23110            let mut items_and_having: Vec<&Expr> = Vec::new();
23111            for it in &u.items {
23112                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23113                    items_and_having.push(expr);
23114                }
23115            }
23116            if let Some(h) = &u.having {
23117                items_and_having.push(h);
23118            }
23119            for e in items_and_having {
23120                if expr_has_toplevel_aggregate(e) {
23121                    return Err(self.err(String::from(
23122                        "aggregate functions are not allowed in a recursive query's recursive term",
23123                    )));
23124                }
23125            }
23126        }
23127        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23128        // subquery) anywhere in the body is rejected; a plain FROM derived
23129        // table is legal in PG and untouched here.
23130        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23131        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23132        for term in all_terms {
23133            if select_has_self_ref_in_sublink(term, &cte.name) {
23134                return Err(self.err(alloc::format!(
23135                    "recursive reference to query \"{}\" must not appear within a subquery",
23136                    cte.name
23137                )));
23138            }
23139        }
23140        Ok(())
23141    }
23142
23143    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23144    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23145    /// right after parse so the engine sees a plain recursive CTE with the
23146    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23147    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23148    /// text-rendered rows can't provide, and errors honestly.
23149    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23150        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23151        if cte.search.is_none() && cte.cycle.is_none() {
23152            return Ok(());
23153        }
23154        let cte_name = cte.name.clone();
23155        let col_names = cte.column_overrides.clone();
23156        if col_names.is_empty() {
23157            return Err(
23158                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23159            );
23160        }
23161        let search = cte.search.take();
23162        let cycle = cte.cycle.take();
23163        let mut extra_cols: Vec<String> = Vec::new();
23164        let col_ref = |name: &str| {
23165            Expr::Column(ColumnName {
23166                qualifier: Some(cte_name.clone()),
23167                name: name.to_string(),
23168            })
23169        };
23170        // Position of a SEARCH/CYCLE column within the CTE's column list.
23171        let pos_of = |name: &str| -> Result<usize, ParseError> {
23172            col_names
23173                .iter()
23174                .position(|c| c.eq_ignore_ascii_case(name))
23175                .ok_or_else(|| {
23176                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23177                })
23178        };
23179        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23180            let mut args = Vec::with_capacity(positions.len());
23181            for &p in positions {
23182                match items.get(p) {
23183                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23184                    _ => {
23185                        return Err(self.err(
23186                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23187                        ));
23188                    }
23189                }
23190            }
23191            Ok(Expr::FunctionCall {
23192                name: "row".into(),
23193                args,
23194            })
23195        };
23196        let CteBody::Select(body) = &mut cte.body else {
23197            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23198        };
23199        if body.unions.is_empty() {
23200            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23201        }
23202        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23203
23204        if let Some(srch) = search {
23205            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23206            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23207            // no typed `record[]`, but element-wise array ORDER BY is correct
23208            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23209            // exactly onto a typed array: DEPTH is the root→node path
23210            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23211            // orders numerically (multi-digit keys included), matching PG.
23212            //
23213            // A multi-column BY would need a record[] to keep the per-node key
23214            // tuple orderable, which SPG can't express — error honestly there
23215            // rather than mis-order.
23216            if srch.by_columns.len() != 1 {
23217                return Err(self.err(
23218                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23219                     SPG doesn't have yet; a single BY column is supported"
23220                        .into(),
23221                ));
23222            }
23223            let key_pos = pos_of(&srch.by_columns[0])?;
23224            let base_key = match body.items.get(key_pos) {
23225                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23226                _ => {
23227                    return Err(
23228                        self.err("SEARCH BY column maps to a non-expression select item".into())
23229                    );
23230                }
23231            };
23232            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23233                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23234                _ => {
23235                    return Err(
23236                        self.err("SEARCH BY column maps to a non-expression select item".into())
23237                    );
23238                }
23239            };
23240            if srch.depth_first {
23241                // base: ARRAY[key]; rec: array_append(cte.set, key).
23242                body.items.push(SelectItem::Expr {
23243                    expr: Expr::Array(alloc::vec![base_key]),
23244                    alias: Some(srch.set_column.clone()),
23245                });
23246                body.unions[rec].1.items.push(SelectItem::Expr {
23247                    expr: Expr::FunctionCall {
23248                        name: "array_append".into(),
23249                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23250                    },
23251                    alias: Some(srch.set_column.clone()),
23252                });
23253            } else {
23254                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23255                // leading depth element dominates the element-wise comparison,
23256                // so shallower rows sort first, then by key — PG's (depth, key).
23257                body.items.push(SelectItem::Expr {
23258                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23259                    alias: Some(srch.set_column.clone()),
23260                });
23261                // rec depth = cte.set[1] + 1.
23262                let parent_depth = Expr::ArraySubscript {
23263                    target: Box::new(col_ref(&srch.set_column)),
23264                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23265                };
23266                body.unions[rec].1.items.push(SelectItem::Expr {
23267                    expr: Expr::Array(alloc::vec![
23268                        Expr::Binary {
23269                            lhs: Box::new(parent_depth),
23270                            op: BinOp::Add,
23271                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23272                        },
23273                        rec_key,
23274                    ]),
23275                    alias: Some(srch.set_column.clone()),
23276                });
23277            }
23278            extra_cols.push(srch.set_column);
23279        }
23280
23281        if let Some(cyc) = cycle {
23282            let positions: Vec<usize> = cyc
23283                .columns
23284                .iter()
23285                .map(|c| pos_of(c))
23286                .collect::<Result<_, _>>()?;
23287            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23288            // cast it to text for the cycle path: membership only needs equality,
23289            // and the record text form gives SPG a TextArray path (SPG has no
23290            // typed record[] array). Cycle detection is unaffected.
23291            let base_row = Expr::Cast {
23292                expr: Box::new(row_of(&body.items, &positions)?),
23293                target: CastTarget::Text,
23294            };
23295            let rec_row = Expr::Cast {
23296                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23297                target: CastTarget::Text,
23298            };
23299            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23300            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23301            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23302            body.items.push(SelectItem::Expr {
23303                expr: Expr::Literal(dflt.clone()),
23304                alias: Some(cyc.mark_column.clone()),
23305            });
23306            body.items.push(SelectItem::Expr {
23307                expr: Expr::Array(alloc::vec![base_row]),
23308                alias: Some(cyc.path_column.clone()),
23309            });
23310            // rec mark: ROW(cols) already in the path → cycle.
23311            let hit = Expr::AnyAll {
23312                expr: Box::new(rec_row.clone()),
23313                op: BinOp::Eq,
23314                array: Box::new(col_ref(&cyc.path_column)),
23315                is_any: true,
23316            };
23317            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23318                Expr::Case {
23319                    operand: None,
23320                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23321                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23322                }
23323            } else {
23324                hit
23325            };
23326            body.unions[rec].1.items.push(SelectItem::Expr {
23327                expr: mark_expr,
23328                alias: Some(cyc.mark_column.clone()),
23329            });
23330            // rec path: array_append(cte.path, ROW(cols)).
23331            body.unions[rec].1.items.push(SelectItem::Expr {
23332                expr: Expr::FunctionCall {
23333                    name: "array_append".into(),
23334                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23335                },
23336                alias: Some(cyc.path_column.clone()),
23337            });
23338            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23339            let stop = Expr::Unary {
23340                op: UnOp::Not,
23341                expr: Box::new(col_ref(&cyc.mark_column)),
23342            };
23343            let w = &mut body.unions[rec].1.where_;
23344            *w = Some(match w.take() {
23345                Some(prev) => Expr::Binary {
23346                    lhs: Box::new(prev),
23347                    op: BinOp::And,
23348                    rhs: Box::new(stop),
23349                },
23350                None => stop,
23351            });
23352            extra_cols.push(cyc.mark_column);
23353            extra_cols.push(cyc.path_column);
23354        }
23355        cte.column_overrides.extend(extra_cols);
23356        Ok(())
23357    }
23358
23359    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23360    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23361    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23362        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23363            return Ok(None);
23364        }
23365        self.advance(); // SEARCH
23366        let depth_first = match self.peek() {
23367            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23368            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23369            other => {
23370                return Err(self.err(format!(
23371                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23372                )));
23373            }
23374        };
23375        self.advance();
23376        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23377            return Err(self.err(format!(
23378                "expected FIRST after SEARCH mode, got {:?}",
23379                self.peek()
23380            )));
23381        }
23382        self.advance();
23383        if !self.peek_is_by() {
23384            return Err(self.err(format!(
23385                "expected BY after SEARCH … FIRST, got {:?}",
23386                self.peek()
23387            )));
23388        }
23389        self.advance();
23390        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23391        while matches!(self.peek(), Token::Comma) {
23392            self.advance();
23393            by_columns.push(self.expect_ident_like()?);
23394        }
23395        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23396            return Err(self.err(format!(
23397                "expected SET in SEARCH clause, got {:?}",
23398                self.peek()
23399            )));
23400        }
23401        self.advance();
23402        let set_column = self.expect_ident_like()?;
23403        Ok(Some(crate::ast::SearchClause {
23404            depth_first,
23405            by_columns,
23406            set_column,
23407        }))
23408    }
23409
23410    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23411    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23412    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23413        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23414            return Ok(None);
23415        }
23416        self.advance(); // CYCLE
23417        let mut columns = alloc::vec![self.expect_ident_like()?];
23418        while matches!(self.peek(), Token::Comma) {
23419            self.advance();
23420            columns.push(self.expect_ident_like()?);
23421        }
23422        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23423            return Err(self.err(format!(
23424                "expected SET in CYCLE clause, got {:?}",
23425                self.peek()
23426            )));
23427        }
23428        self.advance();
23429        let mark_column = self.expect_ident_like()?;
23430        let mut mark_value = None;
23431        let mut default_value = None;
23432        if matches!(self.peek(), Token::To) {
23433            self.advance();
23434            mark_value = Some(self.parse_cycle_literal()?);
23435            if !matches!(self.peek(), Token::Default) {
23436                return Err(self.err(format!(
23437                    "expected DEFAULT after CYCLE … TO, got {:?}",
23438                    self.peek()
23439                )));
23440            }
23441            self.advance();
23442            default_value = Some(self.parse_cycle_literal()?);
23443        }
23444        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23445            return Err(self.err(format!(
23446                "expected USING in CYCLE clause, got {:?}",
23447                self.peek()
23448            )));
23449        }
23450        self.advance();
23451        let path_column = self.expect_ident_like()?;
23452        Ok(Some(crate::ast::CycleClause {
23453            columns,
23454            mark_column,
23455            mark_value,
23456            default_value,
23457            path_column,
23458        }))
23459    }
23460
23461    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23462    /// literal (string / bool / number) in PG.
23463    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23464        match self.parse_expr(0)? {
23465            Expr::Literal(l) => Ok(l),
23466            other => Err(self.err(format!(
23467                "CYCLE mark/default value must be a literal, got {other:?}"
23468            ))),
23469        }
23470    }
23471
23472    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23473        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23474        // Comes through as an identifier; consume it if present and
23475        // mark every CTE in the clause as recursive (PG semantics —
23476        // the flag is per-WITH, not per-CTE).
23477        let mut recursive = false;
23478        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23479            && s.eq_ignore_ascii_case("recursive")
23480        {
23481            self.advance();
23482            recursive = true;
23483        }
23484        let mut ctes = Vec::new();
23485        loop {
23486            let name = self.expect_ident_like()?;
23487            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23488            // PG uses these to rename the body's output columns; we
23489            // do the same below by overriding `columns[i].name`.
23490            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23491                self.advance();
23492                let mut names = Vec::new();
23493                loop {
23494                    names.push(self.expect_ident_like()?);
23495                    if matches!(self.peek(), Token::Comma) {
23496                        self.advance();
23497                        continue;
23498                    }
23499                    break;
23500                }
23501                if !matches!(self.peek(), Token::RParen) {
23502                    return Err(self.err(format!(
23503                        "expected ')' to close CTE column list, got {:?}",
23504                        self.peek()
23505                    )));
23506                }
23507                self.advance();
23508                names
23509            } else {
23510                Vec::new()
23511            };
23512            // AS is a reserved Token::As (used by SELECT-item / FROM
23513            // aliasing) — handle it specially rather than as a bare
23514            // ident.
23515            if !matches!(self.peek(), Token::As) {
23516                return Err(self.err(format!(
23517                    "expected AS after CTE name {name:?}, got {:?}",
23518                    self.peek()
23519                )));
23520            }
23521            self.advance();
23522            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23523            // MATERIALIZED` optimizer hints. SPG materialises every
23524            // CTE, so both spellings are accepted and absorbed.
23525            if matches!(self.peek(), Token::Not) {
23526                self.advance(); // NOT
23527                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23528                    if s.eq_ignore_ascii_case("materialized"))
23529                {
23530                    self.advance();
23531                } else {
23532                    return Err(self.err(format!(
23533                        "expected MATERIALIZED after AS NOT, got {:?}",
23534                        self.peek()
23535                    )));
23536                }
23537            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23538                if s.eq_ignore_ascii_case("materialized"))
23539            {
23540                self.advance();
23541            }
23542            if !matches!(self.peek(), Token::LParen) {
23543                return Err(self.err(format!(
23544                    "expected '(' after AS in WITH clause, got {:?}",
23545                    self.peek()
23546                )));
23547            }
23548            self.advance();
23549            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23550            // RETURNING) as the CTE body in addition to SELECT.
23551            // PG writable CTE semantics. UPDATE / DELETE come in as
23552            // bare Idents (lexer keeps SELECT / INSERT as reserved
23553            // tokens but treats the rest of DML as case-insensitive
23554            // idents).
23555            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23556            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23557            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23558            let body = match self.peek() {
23559                Token::Select => {
23560                    let inner = self.parse_select_stmt()?;
23561                    let Statement::Select(s) = inner else {
23562                        unreachable!("parse_select_stmt returns Select");
23563                    };
23564                    crate::ast::CteBody::Select(s)
23565                }
23566                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23567                // `SELECT * FROM t` this way and accepts it wherever a
23568                // SELECT goes, so the CTE body dispatch needs its own
23569                // arm: this match is keyed on the FIRST token, and
23570                // `Token::Table` fell through to a tail that then
23571                // rejected what it got. `parse_table_shorthand` has
23572                // returned a desugared SelectStatement since the
23573                // shorthand landed — only the routing was missing.
23574                // Round 868 found this by putting the shorthand in a
23575                // subquery; every earlier check used a top-level form.
23576                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23577                // `SELECT * FROM t` this way and accepts it wherever a
23578                // SELECT goes, so the CTE body dispatch needs its own
23579                // arm: this match is keyed on the FIRST token, and
23580                // `Token::Table` fell through to a tail that rejected
23581                // what it got. `parse_table_shorthand` has returned a
23582                // desugared SelectStatement since the shorthand landed —
23583                // only the routing was missing, here and in the derived
23584                // table's second-token gate. Round 868 found both by
23585                // putting the shorthand in a subquery; every earlier
23586                // check had used a top-level form.
23587                Token::Table
23588                    if matches!(
23589                        self.tokens.get(self.pos + 1),
23590                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23591                    ) =>
23592                {
23593                    let mut head = self.parse_table_shorthand()?;
23594                    self.parse_setop_chain_into(&mut head)?;
23595                    self.parse_select_tail_into(&mut head)?;
23596                    crate::ast::CteBody::Select(head)
23597                }
23598                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23599                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23600                // the shared rows helper onto a Select body.
23601                Token::Values => {
23602                    self.advance(); // VALUES
23603                    let mut head = self.parse_values_rows_body()?;
23604                    // A VALUES seed can head a set-operation chain —
23605                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23606                    // SELECT n+1 FROM t …). Attach any trailing
23607                    // UNION / INTERSECT / EXCEPT peers so the
23608                    // recursive-CTE body parses like the SELECT seed.
23609                    self.parse_setop_chain_into(&mut head)?;
23610                    crate::ast::CteBody::Select(head)
23611                }
23612                Token::Insert => {
23613                    let inner = self.parse_one_statement()?;
23614                    let Statement::Insert(s) = inner else {
23615                        unreachable!("Token::Insert routes to Insert");
23616                    };
23617                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23618                }
23619                _ if is_update_kw => {
23620                    let inner = self.parse_one_statement()?;
23621                    let Statement::Update(s) = inner else {
23622                        return Err(
23623                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23624                        );
23625                    };
23626                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23627                }
23628                _ if is_delete_kw => {
23629                    let inner = self.parse_one_statement()?;
23630                    let Statement::Delete(s) = inner else {
23631                        return Err(
23632                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23633                        );
23634                    };
23635                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23636                }
23637                // v7.39 (round 149) — PG 17 allows MERGE as a
23638                // data-modifying CTE body.
23639                _ if is_merge_kw => {
23640                    let inner = self.parse_one_statement()?;
23641                    let Statement::Merge(s) = inner else {
23642                        return Err(
23643                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23644                        );
23645                    };
23646                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23647                }
23648                // v7.39 (round 151) — a CTE body may itself be
23649                // WITH-headed (PG grammar: PreparableStmt carries its
23650                // own with_clause). The nested statement keeps its own
23651                // ctes; the modifying-CTE-at-top-level rule is enforced
23652                // at execution.
23653                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23654                    self.advance(); // WITH
23655                    match self.parse_with_cte_then_select()? {
23656                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23657                        Statement::Insert(s) => {
23658                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23659                        }
23660                        Statement::Update(s) => {
23661                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23662                        }
23663                        Statement::Delete(s) => {
23664                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23665                        }
23666                        Statement::Merge(s) => {
23667                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23668                        }
23669
23670                        other => {
23671                            return Err(self.err(format!(
23672                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23673                            )));
23674                        }
23675                    }
23676                }
23677                other => {
23678                    return Err(self.err(format!(
23679                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23680                    )));
23681                }
23682            };
23683            if !matches!(self.peek(), Token::RParen) {
23684                return Err(self.err(format!(
23685                    "expected ')' after CTE body, got {:?}",
23686                    self.peek()
23687                )));
23688            }
23689            self.advance();
23690            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23691            // CTE, desugared into extra body columns by the engine.
23692            let search = self.parse_cte_search_clause()?;
23693            let cycle = self.parse_cte_cycle_clause()?;
23694            let mut cte = crate::ast::Cte {
23695                name,
23696                body,
23697                recursive,
23698                column_overrides,
23699                search,
23700                cycle,
23701            };
23702            self.validate_recursive_cte(&cte)?;
23703            self.desugar_cte_search_cycle(&mut cte)?;
23704            ctes.push(cte);
23705            if matches!(self.peek(), Token::Comma) {
23706                self.advance();
23707                continue;
23708            }
23709            break;
23710        }
23711        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23712        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23713        // the parsed CTEs to whichever statement the body produces.
23714        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23715        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23716        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23717        match self.peek() {
23718            Token::Select => {
23719                let body_stmt = self.parse_select_stmt()?;
23720                let Statement::Select(mut body) = body_stmt else {
23721                    unreachable!()
23722                };
23723                body.ctes = ctes;
23724                Ok(Statement::Select(body))
23725            }
23726            Token::Insert => {
23727                let body_stmt = self.parse_one_statement()?;
23728                let Statement::Insert(mut body) = body_stmt else {
23729                    unreachable!()
23730                };
23731                body.ctes = ctes;
23732                Ok(Statement::Insert(body))
23733            }
23734            _ if outer_is_update => {
23735                let body_stmt = self.parse_one_statement()?;
23736                let Statement::Update(mut body) = body_stmt else {
23737                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23738                };
23739                body.ctes = ctes;
23740                Ok(Statement::Update(body))
23741            }
23742            _ if outer_is_delete => {
23743                let body_stmt = self.parse_one_statement()?;
23744                let Statement::Delete(mut body) = body_stmt else {
23745                    return Err(self.err(format!("expected DELETE after WITH clause")));
23746                };
23747                body.ctes = ctes;
23748                Ok(Statement::Delete(body))
23749            }
23750            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23751            // WITH RECURSIVE is rejected with PG's exact message
23752            // (parse analysis, transformWithClause).
23753            _ if outer_is_merge => {
23754                if recursive {
23755                    return Err(self.err(String::from(
23756                        "WITH RECURSIVE is not supported for MERGE statement",
23757                    )));
23758                }
23759                let body_stmt = self.parse_one_statement()?;
23760                let Statement::Merge(mut body) = body_stmt else {
23761                    return Err(self.err(format!("expected MERGE after WITH clause")));
23762                };
23763                body.ctes = ctes;
23764                Ok(Statement::Merge(body))
23765            }
23766            other => Err(self.err(format!(
23767                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23768            ))),
23769        }
23770    }
23771
23772    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23773    /// already consumed the leading `EXISTS` ident via
23774    /// `self.advance()`.
23775    /// v7.13.0 — parse the rest of a `CASE … END` expression after
23776    /// the leading `CASE` ident has been consumed (mailrs round-5
23777    /// G9). Supports both the searched form
23778    /// (`CASE WHEN cond THEN val …`) and the simple form
23779    /// (`CASE operand WHEN val THEN val …`).
23780    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23781        // Disambiguate searched vs simple form: if the next token
23782        // is `WHEN`, we're in the searched form. Otherwise the
23783        // intervening expression is the operand.
23784        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23785            None
23786        } else {
23787            Some(Box::new(self.parse_expr(0)?))
23788        };
23789        let mut branches: Vec<(Expr, Expr)> = Vec::new();
23790        loop {
23791            match self.peek() {
23792                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23793                    self.advance();
23794                    let cond = self.parse_expr(0)?;
23795                    match self.peek() {
23796                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23797                            self.advance();
23798                        }
23799                        other => {
23800                            return Err(self.err(alloc::format!(
23801                                "expected THEN after CASE WHEN <expr>, got {other:?}"
23802                            )));
23803                        }
23804                    }
23805                    let value = self.parse_expr(0)?;
23806                    branches.push((cond, value));
23807                }
23808                _ => break,
23809            }
23810        }
23811        if branches.is_empty() {
23812            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23813        }
23814        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23815        {
23816            self.advance();
23817            Some(Box::new(self.parse_expr(0)?))
23818        } else {
23819            None
23820        };
23821        match self.peek() {
23822            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23823                self.advance();
23824            }
23825            other => {
23826                return Err(self.err(alloc::format!(
23827                    "expected END to close CASE expression, got {other:?}"
23828                )));
23829            }
23830        }
23831        Ok(Expr::Case {
23832            operand,
23833            branches,
23834            else_branch,
23835        })
23836    }
23837
23838    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23839    /// query-source position (EXISTS / IN / INSERT source / CTE body /
23840    /// view body). Caller consumed the WITH keyword. Only a SELECT
23841    /// outer is grammatical here; the data-modifying-CTE-at-top-level
23842    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23843    /// maps correctly.
23844    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23845        let inner = self.parse_with_cte_then_select()?;
23846        match inner {
23847            Statement::Select(s) => Ok(s),
23848            other => Err(self.err(format!(
23849                "expected SELECT after WITH in a subquery, got {other:?}"
23850            ))),
23851        }
23852    }
23853
23854    /// True when the next token is the (unquoted) WITH keyword. WITH is
23855    /// reserved in PG, so a bare `with` can never be a column reference
23856    /// in these positions; a quoted `"with"` stays an identifier.
23857    fn peek_is_with_kw(&self) -> bool {
23858        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23859    }
23860
23861    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23862    /// `#[inline(never)]` keeps the large SelectStatement temporaries
23863    /// off parse_expr's recursive frame (the nesting-budget stack
23864    /// cliff — see the round-153 gate regression).
23865    #[inline(never)]
23866    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23867        if self.peek_is_with_kw() {
23868            self.advance();
23869            self.parse_nested_with_select()
23870        } else {
23871            match self.parse_select_stmt()? {
23872                Statement::Select(s) => Ok(s),
23873                other => Err(self.err(alloc::format!(
23874                    "expected SELECT inside ANY/ALL, got {other:?}"
23875                ))),
23876            }
23877        }
23878    }
23879
23880    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23881        if !matches!(self.peek(), Token::LParen) {
23882            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23883        }
23884        self.advance();
23885        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23886        let s = if self.peek_is_with_kw() {
23887            self.advance();
23888            self.parse_nested_with_select()?
23889        } else {
23890            let inner = self.parse_select_stmt()?;
23891            let Statement::Select(s) = inner else {
23892                unreachable!("parse_select_stmt returns Select")
23893            };
23894            s
23895        };
23896        if !matches!(self.peek(), Token::RParen) {
23897            return Err(self.err(format!(
23898                "expected ')' after EXISTS-subquery, got {:?}",
23899                self.peek()
23900            )));
23901        }
23902        self.advance();
23903        Ok(Expr::Exists {
23904            subquery: Box::new(s),
23905            negated,
23906        })
23907    }
23908
23909    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23910        self.advance(); // IN
23911        if !matches!(self.peek(), Token::LParen) {
23912            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23913        }
23914        self.advance();
23915        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23916        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23917        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23918            let s = if self.peek_is_with_kw() {
23919                self.advance();
23920                self.parse_nested_with_select()?
23921            } else {
23922                let inner = self.parse_select_stmt()?;
23923                let Statement::Select(s) = inner else {
23924                    unreachable!("parse_select_stmt always returns Statement::Select")
23925                };
23926                s
23927            };
23928            if !matches!(self.peek(), Token::RParen) {
23929                return Err(self.err(format!(
23930                    "expected ')' after IN-subquery, got {:?}",
23931                    self.peek()
23932                )));
23933            }
23934            self.advance();
23935            return Ok(Expr::InSubquery {
23936                expr: Box::new(expr),
23937                subquery: Box::new(s),
23938                negated,
23939            });
23940        }
23941        let mut elements = Vec::new();
23942        if !matches!(self.peek(), Token::RParen) {
23943            loop {
23944                elements.push(self.parse_expr(0)?);
23945                match self.peek() {
23946                    Token::Comma => {
23947                        self.advance();
23948                    }
23949                    Token::RParen => break,
23950                    other => {
23951                        return Err(
23952                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23953                        );
23954                    }
23955                }
23956            }
23957        }
23958        self.advance(); // ')'
23959        // v7.30.2 (mailrs round-25) — flat InList node instead of a
23960        // left-deep OR-Eq chain: chain depth scaled with the element
23961        // count and overflowed the stack (eval + drop are recursive).
23962        if elements.is_empty() {
23963            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23964        }
23965        Ok(Expr::InList {
23966            expr: Box::new(expr),
23967            list: elements,
23968            negated,
23969        })
23970    }
23971
23972    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23973    /// already consumed by the caller. Elements must be numeric literals
23974    /// (with optional unary `-`); any compound expression is rejected at
23975    /// parse time so the runtime never needs to evaluate inside a vector.
23976    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23977    /// has already consumed the `EXTRACT` token before calling us —
23978    /// we pick up at the opening `(`.
23979    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23980    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23981    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23982    /// per-column OR-fold of
23983    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23984    /// term)` so the existing FTS evaluator handles semantics.
23985    ///
23986    /// The mode modifier is accepted-and-ignored at v7.17 — all
23987    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23988    /// mode operators (`+foo -bar`) would need their own parser
23989    /// (Phase 2.2c); customers who hit them today already get a
23990    /// correct lexeme-match against the bare term, only without
23991    /// the +/- precedence the customer asked for.
23992    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23993        // Already at `MATCH`-consumed position; the dispatcher
23994        // confirmed the next token is `(`.
23995        if !matches!(self.peek(), Token::LParen) {
23996            return Err(self.err(alloc::format!(
23997                "expected '(' after MATCH, got {:?}",
23998                self.peek()
23999            )));
24000        }
24001        self.advance();
24002        let mut cols: Vec<Expr> = Vec::new();
24003        loop {
24004            cols.push(self.parse_expr(0)?);
24005            match self.peek() {
24006                Token::Comma => {
24007                    self.advance();
24008                }
24009                Token::RParen => break,
24010                other => {
24011                    return Err(self.err(alloc::format!(
24012                        "expected ',' or ')' in MATCH column list, got {other:?}"
24013                    )));
24014                }
24015            }
24016        }
24017        self.advance(); // ')'
24018        // Expect AGAINST.
24019        match self.peek() {
24020            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24021                self.advance();
24022            }
24023            other => {
24024                return Err(self.err(alloc::format!(
24025                    "expected AGAINST after MATCH column list, got {other:?}"
24026                )));
24027            }
24028        }
24029        if !matches!(self.peek(), Token::LParen) {
24030            return Err(self.err(alloc::format!(
24031                "expected '(' after AGAINST, got {:?}",
24032                self.peek()
24033            )));
24034        }
24035        self.advance();
24036        // Read AGAINST's argument as a single primary token —
24037        // string literal, placeholder, or column-ref ident. We
24038        // can't call `parse_expr` / `parse_unary` here because
24039        // the postfix chain inside `parse_atom` would greedily
24040        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24041        // and fail at "expected '(' after IN". Customers always
24042        // write a literal or bound parameter in AGAINST, so this
24043        // restriction is non-blocking; the error path explains
24044        // the limit if a more complex expression shows up.
24045        let term = match self.advance() {
24046            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24047            Token::Placeholder(n) => Expr::Placeholder(n),
24048            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24049                qualifier: None,
24050                name: s,
24051            }),
24052            other => {
24053                return Err(self.err(alloc::format!(
24054                    "MATCH ... AGAINST(<term>) expects a string literal, \
24055                     bound parameter, or column ref, got {other:?}"
24056                )));
24057            }
24058        };
24059        // Optional mode tail — accept-and-ignore at v7.17:
24060        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24061        //   IN BOOLEAN MODE
24062        //   WITH QUERY EXPANSION
24063        loop {
24064            match self.peek() {
24065                // IN lexes as a reserved Token::In, not an ident,
24066                // so it gets its own arm.
24067                Token::In => {
24068                    self.advance();
24069                }
24070                Token::Ident(s) | Token::QuotedIdent(s)
24071                    if s.eq_ignore_ascii_case("natural")
24072                        || s.eq_ignore_ascii_case("language")
24073                        || s.eq_ignore_ascii_case("boolean")
24074                        || s.eq_ignore_ascii_case("mode")
24075                        || s.eq_ignore_ascii_case("with")
24076                        || s.eq_ignore_ascii_case("query")
24077                        || s.eq_ignore_ascii_case("expansion") =>
24078                {
24079                    self.advance();
24080                }
24081                _ => break,
24082            }
24083        }
24084        if !matches!(self.peek(), Token::RParen) {
24085            return Err(self.err(alloc::format!(
24086                "expected ')' to close AGAINST, got {:?}",
24087                self.peek()
24088            )));
24089        }
24090        self.advance();
24091        // Build per-column `to_tsvector('simple', col) @@
24092        // plainto_tsquery('simple', term)` and OR-fold.
24093        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24094        let plainto = Expr::FunctionCall {
24095            name: String::from("plainto_tsquery"),
24096            args: alloc::vec![simple_lit(), term.clone()],
24097        };
24098        let mut folded: Option<Expr> = None;
24099        for col in cols {
24100            let to_tsv = Expr::FunctionCall {
24101                name: String::from("to_tsvector"),
24102                args: alloc::vec![simple_lit(), col],
24103            };
24104            let leaf = Expr::Binary {
24105                lhs: Box::new(to_tsv),
24106                op: crate::ast::BinOp::TsMatch,
24107                rhs: Box::new(plainto.clone()),
24108            };
24109            folded = Some(match folded {
24110                None => leaf,
24111                Some(prev) => Expr::Binary {
24112                    lhs: Box::new(prev),
24113                    op: crate::ast::BinOp::Or,
24114                    rhs: Box::new(leaf),
24115                },
24116            });
24117        }
24118        match folded {
24119            Some(e) => Ok(e),
24120            None => Err(self.err(String::from(
24121                "MATCH(...) AGAINST(...) requires at least one column",
24122            ))),
24123        }
24124    }
24125
24126    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24127        if !matches!(self.peek(), Token::LParen) {
24128            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24129        }
24130        self.advance();
24131        let field_name = self.expect_ident_like()?;
24132        let field = match field_name.to_ascii_lowercase().as_str() {
24133            // PG accepts the plural spellings (years/months/…/millenniums) as
24134            // aliases for the singular fields — its datetime unit table has both.
24135            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24136            "year" | "years" => ExtractField::Year,
24137            "month" | "months" => ExtractField::Month,
24138            "day" | "days" => ExtractField::Day,
24139            "hour" | "hours" => ExtractField::Hour,
24140            "minute" | "minutes" => ExtractField::Minute,
24141            "second" | "seconds" => ExtractField::Second,
24142            "microsecond" | "microseconds" => ExtractField::Microsecond,
24143            "epoch" => ExtractField::Epoch,
24144            "dow" => ExtractField::Dow,
24145            "isodow" => ExtractField::Isodow,
24146            "doy" => ExtractField::Doy,
24147            "week" | "weeks" => ExtractField::Week,
24148            "isoyear" => ExtractField::Isoyear,
24149            "quarter" => ExtractField::Quarter,
24150            "decade" | "decades" => ExtractField::Decade,
24151            "century" | "centuries" => ExtractField::Century,
24152            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24153            "julian" => ExtractField::Julian,
24154            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24155            "timezone" => ExtractField::Timezone,
24156            "timezone_hour" => ExtractField::TimezoneHour,
24157            "timezone_minute" => ExtractField::TimezoneMinute,
24158            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24159            // reports an unknown one with the source type (22023); carry the
24160            // raw name so eval can word it.
24161            other => ExtractField::Other(alloc::string::String::from(other)),
24162        };
24163        if !matches!(self.peek(), Token::From) {
24164            return Err(self.err(format!(
24165                "expected FROM after EXTRACT field, got {:?}",
24166                self.peek()
24167            )));
24168        }
24169        self.advance();
24170        let source = self.parse_expr(0)?;
24171        if !matches!(self.peek(), Token::RParen) {
24172            return Err(self.err(format!(
24173                "expected ')' to close EXTRACT, got {:?}",
24174                self.peek()
24175            )));
24176        }
24177        self.advance();
24178        Ok(Expr::Extract {
24179            field,
24180            source: Box::new(source),
24181        })
24182    }
24183
24184    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24185    /// is already consumed; we expect a single string literal next and
24186    /// resolve it into `Literal::Interval` at parse time so the engine
24187    /// never has to re-tokenise inside the string.
24188    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24189    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24190    /// is the SQL-standard form and is left to the path below.
24191    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24192        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24193        let (offset, sign) = match self.peek() {
24194            Token::Minus => (1, "-"),
24195            _ => (0, ""),
24196        };
24197        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24198            return None;
24199        };
24200        self.tokens
24201            .get(self.pos + offset + 1)
24202            .filter(|t| mysql_interval_unit(t).is_some())?;
24203        Some((alloc::format!("{sign}{n}"), offset + 1))
24204    }
24205
24206    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24207    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24208    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24209    ///
24210    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24211    /// this by parsing the group and then restoring `self.pos` — which could
24212    /// never have worked, because `advance()` DESTROYS the token it returns
24213    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24214    /// inert only because both branches errored back then.
24215    fn interval_paren_is_quantity(&self) -> bool {
24216        let mut depth = 0usize;
24217        let mut saw_top_level_comma = false;
24218        let mut i = self.pos;
24219        while let Some(tok) = self.tokens.get(i) {
24220            match tok {
24221                Token::LParen => depth += 1,
24222                Token::RParen => {
24223                    depth = depth.saturating_sub(1);
24224                    if depth == 0 {
24225                        return !saw_top_level_comma
24226                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24227                                .is_some();
24228                    }
24229                }
24230                // A comma directly inside the outermost parens means the
24231                // argument list of the INTERVAL() function.
24232                Token::Comma if depth == 1 => saw_top_level_comma = true,
24233                Token::Eof => return false,
24234                _ => {}
24235            }
24236            i += 1;
24237        }
24238        false
24239    }
24240
24241    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24242        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24243        // (the index of the last Ni ≤ N), distinct from the interval literal.
24244        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24245        // is decided by a non-destructive lookahead (round 422) before either
24246        // branch consumes anything. MySQL only.
24247        if self.mysql_dialect
24248            && matches!(self.peek(), Token::LParen)
24249            && !self.interval_paren_is_quantity()
24250        {
24251            self.advance(); // (
24252            let mut args = Vec::new();
24253            if !matches!(self.peek(), Token::RParen) {
24254                loop {
24255                    args.push(self.parse_expr(0)?);
24256                    if matches!(self.peek(), Token::Comma) {
24257                        self.advance();
24258                        continue;
24259                    }
24260                    break;
24261                }
24262            }
24263            if !matches!(self.peek(), Token::RParen) {
24264                return Err(self.err(alloc::format!(
24265                    "expected ')' after INTERVAL() arguments, got {:?}",
24266                    self.peek()
24267                )));
24268            }
24269            self.advance(); // )
24270            return Ok(Expr::FunctionCall {
24271                name: alloc::string::String::from("interval"),
24272                args,
24273            });
24274        }
24275        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24276        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24277        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24278        // writes every date arithmetic there is, and it did not parse at
24279        // all. PG rejects the unquoted form outright (`syntax error at or
24280        // near "1"`, measured), so it is taken only in the MySQL dialect —
24281        // PG's own `INTERVAL '1' DAY` is untouched below.
24282        if self.mysql_dialect
24283            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24284        {
24285            for _ in 0..consume {
24286                self.advance(); // the optional `-` and the number
24287            }
24288            let Some(unit) = mysql_interval_unit(self.peek()) else {
24289                return Err(self.err(alloc::format!(
24290                    "expected an interval unit after INTERVAL {text}, got {:?}",
24291                    self.peek()
24292                )));
24293            };
24294            self.advance(); // the unit
24295            let (months, days, micros) = scale_mysql_interval(&text, unit)
24296                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24297            return Ok(Expr::Literal(Literal::Interval {
24298                months,
24299                days,
24300                micros,
24301                // The canonical rendering, so Display round-trips into a
24302                // form both dialects read back.
24303                text: alloc::format!("{text} {unit}"),
24304            }));
24305        }
24306        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24307        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24308        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24309        // Those cannot fold into a compile-time `Literal::Interval`, so they
24310        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24311        // builtin, which builds the value at run time (and yields NULL for a
24312        // NULL quantity, as MariaDB does). The literal path above still folds
24313        // the constant case — it is cheaper and round-trips through Display.
24314        //
24315        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24316        // MySQL's quoted spelling) keep the qualifier path below.
24317        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24318            let qty = self.parse_expr(0)?;
24319            let Some(unit) = mysql_interval_unit(self.peek()) else {
24320                return Err(self.err(alloc::format!(
24321                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24322                    self.peek()
24323                )));
24324            };
24325            self.advance(); // the unit
24326            return Ok(make_interval_call(qty, unit));
24327        }
24328        let tok = self.advance();
24329        let Token::String(text) = tok else {
24330            return Err(self.err(format!(
24331                "expected string literal after INTERVAL, got {tok:?}"
24332            )));
24333        };
24334        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24335        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24336        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24337        // bare number means and the leading/trailing precision.
24338        let field1 = interval_field_of(self.peek());
24339        let qualifier = if let Some(f1) = field1 {
24340            self.advance();
24341            let f2 = if matches!(self.peek(), Token::To) {
24342                self.advance();
24343                let Some(f) = interval_field_of(self.peek()) else {
24344                    return Err(self.err(format!(
24345                        "expected an interval field after TO, got {:?}",
24346                        self.peek()
24347                    )));
24348                };
24349                self.advance();
24350                Some(f)
24351            } else {
24352                None
24353            };
24354            Some((f1, f2))
24355        } else {
24356            None
24357        };
24358        let (months, days, micros) = match qualifier {
24359            Some(q) => interpret_qualified_interval(&text, q),
24360            None => parse_interval_text(&text),
24361        }
24362        .ok_or_else(|| ParseError {
24363            message: format!(
24364                "cannot parse INTERVAL {text:?}; \
24365                     expected `<n> <unit> [<n> <unit> ...]` with units \
24366                     microsecond[s], millisecond[s], second[s], minute[s], \
24367                     hour[s], day[s], week[s], month[s], year[s]"
24368            ),
24369            token_pos: self.consumed_pos(),
24370        })?;
24371        Ok(Expr::Literal(Literal::Interval {
24372            months,
24373            days,
24374            micros,
24375            text,
24376        }))
24377    }
24378
24379    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24380    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24381    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24382    /// than a pgvector literal.
24383    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24384        self.advance(); // consume `[`
24385        let mut items: Vec<Expr> = Vec::new();
24386        if !matches!(self.peek(), Token::RBracket) {
24387            loop {
24388                if matches!(self.peek(), Token::LBracket) {
24389                    items.push(self.parse_array_bracket_body()?);
24390                } else {
24391                    items.push(self.parse_expr(0)?);
24392                }
24393                match self.peek() {
24394                    Token::Comma => {
24395                        self.advance();
24396                    }
24397                    Token::RBracket => break,
24398                    other => {
24399                        return Err(self.err(alloc::format!(
24400                            "expected ',' or ']' in array literal, got {other:?}"
24401                        )));
24402                    }
24403                }
24404            }
24405        }
24406        self.advance(); // consume `]`
24407        Ok(Expr::Array(items))
24408    }
24409
24410    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24411        let mut elems = Vec::new();
24412        if matches!(self.peek(), Token::RBracket) {
24413            self.advance();
24414            return Ok(Expr::Literal(Literal::Vector(elems)));
24415        }
24416        loop {
24417            let e = self.parse_expr(0)?;
24418            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24419                message: format!("vector element must be a numeric literal, got {e:?}"),
24420                token_pos: self.pos,
24421            })?;
24422            elems.push(x);
24423            match self.peek() {
24424                Token::Comma => {
24425                    self.advance();
24426                }
24427                Token::RBracket => {
24428                    self.advance();
24429                    break;
24430                }
24431                other => {
24432                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24433                }
24434            }
24435        }
24436        Ok(Expr::Literal(Literal::Vector(elems)))
24437    }
24438
24439    /// Atom that started with an identifier: could be `t.col`, `col`, or
24440    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24441    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24442    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24443    /// is optional; an empty `()` is also legal (PG semantics).
24444    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24445    /// modifier between `name(args)` and `OVER (...)`. Default is
24446    /// `Respect`. Unrecognised idents leave the stream unchanged.
24447    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24448        let Token::Ident(s) = self.peek().clone() else {
24449            return NullTreatment::Respect;
24450        };
24451        let is_ignore = s.eq_ignore_ascii_case("ignore");
24452        let is_respect = s.eq_ignore_ascii_case("respect");
24453        if !is_ignore && !is_respect {
24454            return NullTreatment::Respect;
24455        }
24456        // Lookahead for NULLS — only consume both tokens together.
24457        // pos+1 must hold a "nulls" ident.
24458        if self.pos + 1 < self.tokens.len()
24459            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24460            && s2.eq_ignore_ascii_case("nulls")
24461        {
24462            self.advance();
24463            self.advance();
24464            return if is_ignore {
24465                NullTreatment::Ignore
24466            } else {
24467                NullTreatment::Respect
24468            };
24469        }
24470        NullTreatment::Respect
24471    }
24472
24473    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24474    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24475    /// (same shape as the `OVER` tail). Consumes the whole clause and
24476    /// returns the predicate; returns `None` when no `FILTER` follows.
24477    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24478        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24479            return Ok(None);
24480        };
24481        if !s.eq_ignore_ascii_case("filter") {
24482            return Ok(None);
24483        }
24484        self.advance(); // FILTER
24485        if !matches!(self.peek(), Token::LParen) {
24486            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24487        }
24488        self.advance(); // (
24489        if !matches!(self.peek(), Token::Where) {
24490            return Err(self.err(format!(
24491                "expected WHERE inside FILTER (...), got {:?}",
24492                self.peek()
24493            )));
24494        }
24495        self.advance(); // WHERE
24496        let cond = self.parse_expr(0)?;
24497        if !matches!(self.peek(), Token::RParen) {
24498            return Err(self.err(format!(
24499                "expected ')' to close FILTER (WHERE ...), got {:?}",
24500                self.peek()
24501            )));
24502        }
24503        self.advance(); // )
24504        Ok(Some(Box::new(cond)))
24505    }
24506
24507    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24508    /// the separator as the aggregate's second argument, which is the
24509    /// shape `string_agg` already takes. Returns whether one was there.
24510    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24511        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24512            return Ok(false);
24513        }
24514        self.advance();
24515        let Token::String(sep) = self.peek().clone() else {
24516            return Err(self.err(alloc::format!(
24517                "expected a string literal after SEPARATOR, got {:?}",
24518                self.peek()
24519            )));
24520        };
24521        self.advance();
24522        args.push(Expr::Literal(Literal::String(sep)));
24523        Ok(true)
24524    }
24525
24526    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24527    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24528    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24529    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24530    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24531        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24532            return Ok(Vec::new());
24533        };
24534        if !s.eq_ignore_ascii_case("within") {
24535            return Ok(Vec::new());
24536        }
24537        self.advance(); // WITHIN
24538        if !matches!(self.peek(), Token::Group) {
24539            return Err(self.err(format!(
24540                "expected GROUP after WITHIN, got {:?}",
24541                self.peek()
24542            )));
24543        }
24544        self.advance(); // GROUP
24545        if !matches!(self.peek(), Token::LParen) {
24546            return Err(self.err(format!(
24547                "expected '(' after WITHIN GROUP, got {:?}",
24548                self.peek()
24549            )));
24550        }
24551        self.advance(); // (
24552        if !matches!(self.peek(), Token::Order) {
24553            return Err(self.err(format!(
24554                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24555                self.peek()
24556            )));
24557        }
24558        self.advance(); // ORDER
24559        if !self.peek_is_by() {
24560            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24561        }
24562        self.advance(); // BY
24563        let mut keys: Vec<OrderBy> = Vec::new();
24564        loop {
24565            // v7.39 (round 691) — save/restore, the discipline this parser
24566            // already uses around `pending_sample_preds`, so a subquery inside
24567            // a key neither inherits nor leaks the channel.
24568            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24569            let saved_coll = self.order_key_collation.take();
24570            let parsed = self.parse_expr(0);
24571            self.in_order_by_key = saved_flag;
24572            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24573            let expr = parsed?;
24574            let desc = if matches!(self.peek(), Token::Desc) {
24575                self.advance();
24576                true
24577            } else if matches!(self.peek(), Token::Asc) {
24578                self.advance();
24579                false
24580            } else {
24581                false
24582            };
24583            let nulls_first = self.parse_optional_nulls_placement()?;
24584            keys.push(OrderBy {
24585                expr,
24586                desc,
24587                nulls_first,
24588                collation,
24589            });
24590            if matches!(self.peek(), Token::Comma) {
24591                self.advance();
24592            } else {
24593                break;
24594            }
24595        }
24596        if !matches!(self.peek(), Token::RParen) {
24597            return Err(self.err(format!(
24598                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24599                self.peek()
24600            )));
24601        }
24602        self.advance(); // )
24603        Ok(keys)
24604    }
24605
24606    /// No frame clause is supported.
24607    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24608    fn parse_over_clause(
24609        &mut self,
24610    ) -> Result<
24611        (
24612            Vec<Expr>,
24613            Vec<(Expr, bool, Option<bool>)>,
24614            Option<WindowFrame>,
24615        ),
24616        ParseError,
24617    > {
24618        // `OVER w` — a named-window reference. The WINDOW clause
24619        // parses after the select list, so the name rides out as a
24620        // marker in partition_by; parse_bare_select substitutes the
24621        // definition once the clause is known.
24622        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24623            let name = w.clone();
24624            self.advance();
24625            return Ok((
24626                alloc::vec![Expr::Column(crate::ast::ColumnName {
24627                    qualifier: Some("__named_window__".to_string()),
24628                    name,
24629                })],
24630                Vec::new(),
24631                None,
24632            ));
24633        }
24634        if !matches!(self.peek(), Token::LParen) {
24635            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24636        }
24637        self.advance();
24638        let mut partition_by = Vec::new();
24639        let mut order_by = Vec::new();
24640        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24641        // window, refined in place. PG's rules (probed against 18.4) differ
24642        // from the bare `OVER w1` form, so the reference rides out under its
24643        // own marker and `substitute_named_windows` applies them. The base
24644        // name is any leading identifier that isn't a window-spec keyword.
24645        let base_window = match self.peek() {
24646            Token::Ident(s) | Token::QuotedIdent(s)
24647                if !s.eq_ignore_ascii_case("partition")
24648                    && !s.eq_ignore_ascii_case("rows")
24649                    && !s.eq_ignore_ascii_case("range")
24650                    && !s.eq_ignore_ascii_case("groups") =>
24651            {
24652                let n = s.clone();
24653                self.advance();
24654                Some(n)
24655            }
24656            _ => None,
24657        };
24658        // PARTITION BY ?
24659        // v7.37.6-B promoted PARTITION to a reserved keyword
24660        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24661        // `Token::Ident("partition")`. Accept both so older sources
24662        // and the new lexer surface land on the same path.
24663        let is_partition_kw = match self.peek() {
24664            Token::Partition => true,
24665            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24666            _ => false,
24667        };
24668        if is_partition_kw {
24669            self.advance();
24670            if !self.peek_is_by() {
24671                return Err(self.err(format!(
24672                    "expected BY after PARTITION, got {:?}",
24673                    self.peek()
24674                )));
24675            }
24676            self.advance();
24677            loop {
24678                partition_by.push(self.parse_expr(0)?);
24679                if matches!(self.peek(), Token::Comma) {
24680                    self.advance();
24681                    continue;
24682                }
24683                break;
24684            }
24685        }
24686        // ORDER BY ?
24687        if matches!(self.peek(), Token::Order) {
24688            self.advance();
24689            if !self.peek_is_by() {
24690                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24691            }
24692            self.advance();
24693            loop {
24694                let e = self.parse_expr(0)?;
24695                let desc = if matches!(self.peek(), Token::Desc) {
24696                    self.advance();
24697                    true
24698                } else if matches!(self.peek(), Token::Asc) {
24699                    self.advance();
24700                    false
24701                } else {
24702                    false
24703                };
24704                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24705                let nulls_first = self.parse_optional_nulls_placement()?;
24706                order_by.push((e, desc, nulls_first));
24707                if matches!(self.peek(), Token::Comma) {
24708                    self.advance();
24709                    continue;
24710                }
24711                break;
24712            }
24713        }
24714        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24715        // Both keywords come through the lexer as identifiers; match
24716        // case-insensitively.
24717        let mut frame: Option<WindowFrame> = None;
24718        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24719            let kind = if s.eq_ignore_ascii_case("rows") {
24720                Some(FrameKind::Rows)
24721            } else if s.eq_ignore_ascii_case("range") {
24722                Some(FrameKind::Range)
24723            } else if s.eq_ignore_ascii_case("groups") {
24724                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24725                Some(FrameKind::Groups)
24726            } else {
24727                None
24728            };
24729            if let Some(kind) = kind {
24730                self.advance();
24731                frame = Some(self.parse_frame_tail(kind)?);
24732            }
24733        }
24734        if !matches!(self.peek(), Token::RParen) {
24735            return Err(self.err(format!(
24736                "expected ')' to close OVER clause, got {:?}",
24737                self.peek()
24738            )));
24739        }
24740        self.advance();
24741        if let Some(base) = base_window {
24742            // A copy may refine but never override the base's partitioning
24743            // (PG rejects it outright, before looking the name up).
24744            if !partition_by.is_empty() {
24745                return Err(self.err(alloc::format!(
24746                    "cannot override PARTITION BY clause of window \"{base}\""
24747                )));
24748            }
24749            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24750                qualifier: Some("__named_window_ref__".to_string()),
24751                name: base,
24752            })];
24753        }
24754        Ok((partition_by, order_by, frame))
24755    }
24756
24757    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24758    /// or `RANGE` keyword was just consumed. Accepts both
24759    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24760    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24761    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24762    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24763        let (start, end) = if matches!(self.peek(), Token::Between) {
24764            self.advance();
24765            let start = self.parse_frame_bound()?;
24766            if !matches!(self.peek(), Token::And) {
24767                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24768            }
24769            self.advance();
24770            let end = self.parse_frame_bound()?;
24771            (start, Some(end))
24772        } else {
24773            (self.parse_frame_bound()?, None)
24774        };
24775        let exclude = self.parse_frame_exclusion()?;
24776        Ok(WindowFrame {
24777            kind,
24778            start,
24779            end,
24780            exclude,
24781        })
24782    }
24783
24784    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24785    /// after a frame spec. NO OTHERS is the default no-op.
24786    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24787        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24788            return Ok(FrameExclusion::NoOthers);
24789        }
24790        self.advance(); // EXCLUDE
24791        match self.peek() {
24792            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24793                self.advance();
24794                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24795                    return Err(self.err(format!(
24796                        "expected ROW after EXCLUDE CURRENT, got {:?}",
24797                        self.peek()
24798                    )));
24799                }
24800                self.advance();
24801                Ok(FrameExclusion::CurrentRow)
24802            }
24803            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24804            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24805            // Without this arm it fell to the catch-all, whose message
24806            // self-contradictingly listed GROUP as expected.
24807            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24808                self.advance();
24809                Ok(FrameExclusion::Group)
24810            }
24811            Token::Group => {
24812                self.advance();
24813                Ok(FrameExclusion::Group)
24814            }
24815            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24816                self.advance();
24817                Ok(FrameExclusion::Ties)
24818            }
24819            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24820                self.advance();
24821                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24822                    return Err(self.err(format!(
24823                        "expected OTHERS after EXCLUDE NO, got {:?}",
24824                        self.peek()
24825                    )));
24826                }
24827                self.advance();
24828                Ok(FrameExclusion::NoOthers)
24829            }
24830            other => Err(self.err(format!(
24831                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24832            ))),
24833        }
24834    }
24835
24836    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24837    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24838    /// `UNBOUNDED FOLLOWING`.
24839    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24840        // Interval-typed offset for a value-based RANGE frame over a
24841        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24842        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24843        // PRECEDING`.
24844        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24845            let dir = self.expect_ident_like()?;
24846            return if dir.eq_ignore_ascii_case("preceding") {
24847                Ok(FrameBound::IntervalPreceding {
24848                    months,
24849                    days,
24850                    micros,
24851                })
24852            } else if dir.eq_ignore_ascii_case("following") {
24853                Ok(FrameBound::IntervalFollowing {
24854                    months,
24855                    days,
24856                    micros,
24857                })
24858            } else {
24859                Err(self.err(format!(
24860                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24861                )))
24862            };
24863        }
24864        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24865        if let Token::Integer(n) = *self.peek() {
24866            self.advance();
24867            let n: u64 = u64::try_from(n).map_err(|_| {
24868                self.err(format!(
24869                    "invalid frame offset {n} — expected non-negative integer"
24870                ))
24871            })?;
24872            let dir = self.expect_ident_like()?;
24873            return if dir.eq_ignore_ascii_case("preceding") {
24874                Ok(FrameBound::OffsetPreceding(n))
24875            } else if dir.eq_ignore_ascii_case("following") {
24876                Ok(FrameBound::OffsetFollowing(n))
24877            } else {
24878                Err(self.err(format!(
24879                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24880                )))
24881            };
24882        }
24883        let first = self.expect_ident_like()?;
24884        if first.eq_ignore_ascii_case("unbounded") {
24885            let dir = self.expect_ident_like()?;
24886            return if dir.eq_ignore_ascii_case("preceding") {
24887                Ok(FrameBound::UnboundedPreceding)
24888            } else if dir.eq_ignore_ascii_case("following") {
24889                Ok(FrameBound::UnboundedFollowing)
24890            } else {
24891                Err(self.err(format!(
24892                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24893                )))
24894            };
24895        }
24896        if first.eq_ignore_ascii_case("current") {
24897            let row = self.expect_ident_like()?;
24898            if !row.eq_ignore_ascii_case("row") {
24899                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24900            }
24901            return Ok(FrameBound::CurrentRow);
24902        }
24903        Err(self.err(format!(
24904            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24905        )))
24906    }
24907
24908    /// Detect and consume a leading interval offset in a frame bound —
24909    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24910    /// `(months, days, micros)`. Leaves the cursor on the trailing
24911    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24912    /// when the next tokens are not an interval offset.
24913    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24914        // Shape A — `INTERVAL '1 day'`.
24915        if matches!(self.peek(), Token::Interval) {
24916            self.advance(); // INTERVAL
24917            let atom = self.parse_interval_atom()?;
24918            if let Expr::Literal(Literal::Interval {
24919                months,
24920                days,
24921                micros,
24922                ..
24923            }) = atom
24924            {
24925                return Ok(Some((months, days, micros)));
24926            }
24927            return Err(self.err("expected an interval literal in frame offset".to_string()));
24928        }
24929        // Shape B — `'1 day'::interval`. Look ahead for the exact
24930        // string / `::` / interval-target triple before committing.
24931        if let Token::String(text) = self.peek() {
24932            let target_is_interval = match self.tokens.get(self.pos + 2) {
24933                Some(Token::Interval) => true,
24934                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24935                _ => false,
24936            };
24937            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24938                && target_is_interval;
24939            if is_cast {
24940                let text = text.clone();
24941                self.advance(); // string
24942                self.advance(); // ::
24943                self.advance(); // interval
24944                let parts = parse_interval_text(&text).ok_or_else(|| {
24945                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24946                })?;
24947                return Ok(Some(parts));
24948            }
24949        }
24950        Ok(None)
24951    }
24952
24953    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24954        if matches!(self.peek(), Token::Dot) {
24955            self.advance();
24956            let name = self.expect_ident_like()?;
24957            // v7.14.0 — schema-qualified function call
24958            // `<schema>.<fn>(args)`. PG dumps emit
24959            // `pg_catalog.set_config(...)` in the preamble. SPG
24960            // is single-namespace: drop the schema prefix and
24961            // route the dispatch on the bare function name.
24962            if matches!(self.peek(), Token::LParen) {
24963                return self.finish_ident_atom(name);
24964            }
24965            return Ok(Expr::Column(ColumnName {
24966                qualifier: Some(first),
24967                name,
24968            }));
24969        }
24970        if matches!(self.peek(), Token::LParen) {
24971            self.advance();
24972            // `COUNT(*)` — special-cased here because `*` isn't a normal
24973            // expression token. Lower-case match on `first` since the lexer
24974            // folds identifiers.
24975            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24976                self.advance();
24977                if !matches!(self.peek(), Token::RParen) {
24978                    return Err(self.err(format!(
24979                        "expected ')' after COUNT(*), got {:?}",
24980                        self.peek()
24981                    )));
24982                }
24983                self.advance();
24984                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24985                let filter = self.parse_filter_clause()?;
24986                // v4.12: COUNT(*) OVER (...) — same window tail.
24987                let null_treatment = self.parse_null_treatment_modifier();
24988                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24989                    && s.eq_ignore_ascii_case("over")
24990                {
24991                    self.advance();
24992                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
24993                    return Ok(Expr::WindowFunction {
24994                        name: "count_star".into(),
24995                        args: Vec::new(),
24996                        partition_by,
24997                        order_by,
24998                        frame,
24999                        null_treatment,
25000                        filter,
25001                    });
25002                }
25003                if let Some(filter) = filter {
25004                    return Ok(Expr::AggregateOrdered {
25005                        call: Box::new(Expr::FunctionCall {
25006                            name: "count_star".into(),
25007                            args: Vec::new(),
25008                        }),
25009                        order_by: Vec::new(),
25010                        distinct: false,
25011                        filter: Some(filter),
25012                    });
25013                }
25014                return Ok(Expr::FunctionCall {
25015                    name: "count_star".into(),
25016                    args: Vec::new(),
25017                });
25018            }
25019            // Function call. PG-style: zero-or-more comma-separated args.
25020            let mut args = Vec::new();
25021            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25022            // Names are collected in lock-step with `args` and resolved to
25023            // positional order after the loop (the AST stays positional).
25024            let mut arg_names: Vec<Option<String>> = Vec::new();
25025            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25026            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25027            // seen, so the value arguments before it can be folded.
25028            let mut saw_separator = false;
25029            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25030            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25031            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25032            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25033                self.advance();
25034                true
25035            } else if matches!(self.peek(), Token::All) {
25036                self.advance();
25037                false
25038            } else {
25039                false
25040            };
25041            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25042            // TIMESTAMPDIFF take a bare unit keyword as the first
25043            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25044            // bare type keyword (DATE / TIME / DATETIME); lower them
25045            // onto string literals so the evaluator sees plain text.
25046            if ((first.eq_ignore_ascii_case("timestampadd")
25047                || first.eq_ignore_ascii_case("timestampdiff"))
25048                && matches!(self.peek(), Token::Ident(u) if matches!(
25049                    u.to_ascii_lowercase().as_str(),
25050                    "microsecond" | "second" | "minute" | "hour" | "day"
25051                        | "week" | "month" | "quarter" | "year"
25052                )))
25053                || (first.eq_ignore_ascii_case("get_format")
25054                    && matches!(self.peek(), Token::Ident(u) if matches!(
25055                        u.to_ascii_lowercase().as_str(),
25056                        "date" | "time" | "datetime" | "timestamp"
25057                    )))
25058            {
25059                if let Token::Ident(u) = self.peek() {
25060                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25061                }
25062                self.advance();
25063                if matches!(self.peek(), Token::Comma) {
25064                    self.advance();
25065                }
25066            }
25067            // `ROW(a, b, …)` keyword constructor. Followed by a
25068            // comparison operator or [NOT] IN it joins the paren
25069            // row-constructor machinery (fieldwise parse-time
25070            // expansion); bare, it stays a `row` call the evaluator
25071            // renders as PG record text.
25072            if first.eq_ignore_ascii_case("row") {
25073                let mut row_items = Vec::new();
25074                if !matches!(self.peek(), Token::RParen) {
25075                    loop {
25076                        row_items.push(self.parse_expr(0)?);
25077                        match self.peek() {
25078                            Token::Comma => {
25079                                self.advance();
25080                            }
25081                            Token::RParen => break,
25082                            other => {
25083                                return Err(self.err(format!(
25084                                    "expected ',' or ')' in ROW(...), got {other:?}"
25085                                )));
25086                            }
25087                        }
25088                    }
25089                }
25090                self.advance(); // ')'
25091                let comparison_follows = matches!(
25092                    self.peek(),
25093                    Token::Eq
25094                        | Token::NotEq
25095                        | Token::Lt
25096                        | Token::LtEq
25097                        | Token::Gt
25098                        | Token::GtEq
25099                        | Token::In
25100                ) || (matches!(self.peek(), Token::Not)
25101                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25102                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25103                if comparison_follows && !row_items.is_empty() {
25104                    return self.parse_row_comparison_tail(row_items);
25105                }
25106                return Ok(Expr::FunctionCall {
25107                    name: String::from("row"),
25108                    args: row_items,
25109                });
25110            }
25111            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25112            // the parse-mode keyword introduces the source text. SPG
25113            // carries XML as text, so both modes lower to __xmlparse(expr)
25114            // which validates well-formedness and returns Value::Xml.
25115            if first.eq_ignore_ascii_case("xmlparse")
25116                && matches!(self.peek(), Token::Ident(kw)
25117                    if kw.eq_ignore_ascii_case("document")
25118                        || kw.eq_ignore_ascii_case("content"))
25119            {
25120                let mode = match self.advance() {
25121                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25122                    _ => unreachable!("peeked an ident"),
25123                };
25124                let src = self.parse_expr(0)?;
25125                if !matches!(self.peek(), Token::RParen) {
25126                    return Err(self.err(format!(
25127                        "expected ')' to close XMLPARSE, got {:?}",
25128                        self.peek()
25129                    )));
25130                }
25131                self.advance();
25132                return Ok(Expr::FunctionCall {
25133                    name: String::from("__xmlparse"),
25134                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25135                });
25136            }
25137            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25138            // keyword introduces the element name (a bare or quoted
25139            // identifier), then optional content expressions. Lower to a
25140            // plain `xmlelement(name_text, content …)` call.
25141            if first.eq_ignore_ascii_case("xmlelement")
25142                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25143            {
25144                self.advance(); // consume NAME
25145                let elem_name = match self.peek().clone() {
25146                    Token::Ident(n) | Token::QuotedIdent(n) => {
25147                        self.advance();
25148                        n
25149                    }
25150                    other => {
25151                        return Err(self.err(format!(
25152                            "expected element name after XMLELEMENT NAME, got {other:?}"
25153                        )));
25154                    }
25155                };
25156                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25157                while matches!(self.peek(), Token::Comma) {
25158                    self.advance();
25159                    args.push(self.parse_expr(0)?);
25160                }
25161                if !matches!(self.peek(), Token::RParen) {
25162                    return Err(self.err(format!(
25163                        "expected ')' to close XMLELEMENT, got {:?}",
25164                        self.peek()
25165                    )));
25166                }
25167                self.advance();
25168                return Ok(Expr::FunctionCall {
25169                    name: String::from("xmlelement"),
25170                    args,
25171                });
25172            }
25173            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25174            // becomes a `<name>value</name>` element; a bare column infers its
25175            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25176            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25177                let mut args: Vec<Expr> = Vec::new();
25178                loop {
25179                    let val = self.parse_expr(0)?;
25180                    let name = if matches!(self.peek(), Token::As) {
25181                        self.advance();
25182                        match self.peek().clone() {
25183                            Token::Ident(n) | Token::QuotedIdent(n) => {
25184                                self.advance();
25185                                n
25186                            }
25187                            other => {
25188                                return Err(self.err(format!(
25189                                    "expected name after AS in XMLFOREST, got {other:?}"
25190                                )));
25191                            }
25192                        }
25193                    } else if let Expr::Column(c) = &val {
25194                        c.name.clone()
25195                    } else {
25196                        return Err(
25197                            self.err("XMLFOREST element without a column name needs AS".into())
25198                        );
25199                    };
25200                    args.push(Expr::Literal(Literal::String(name)));
25201                    args.push(val);
25202                    if matches!(self.peek(), Token::Comma) {
25203                        self.advance();
25204                    } else {
25205                        break;
25206                    }
25207                }
25208                if !matches!(self.peek(), Token::RParen) {
25209                    return Err(self.err(format!(
25210                        "expected ')' to close XMLFOREST, got {:?}",
25211                        self.peek()
25212                    )));
25213                }
25214                self.advance();
25215                return Ok(Expr::FunctionCall {
25216                    name: String::from("xmlforest"),
25217                    args,
25218                });
25219            }
25220            // SQL-standard `POSITION(sub IN str)` — lowers onto
25221            // strpos(str, sub). IN is the argument separator here,
25222            // so the needle parses with the IN-tail suppressed.
25223            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25224                let saved = self.suppress_in_tail;
25225                self.suppress_in_tail = true;
25226                let needle = self.parse_expr(0);
25227                self.suppress_in_tail = saved;
25228                let needle = needle?;
25229                if matches!(self.peek(), Token::In) {
25230                    self.advance();
25231                    let haystack = self.parse_expr(0)?;
25232                    if !matches!(self.peek(), Token::RParen) {
25233                        return Err(self.err(format!(
25234                            "expected ')' to close POSITION, got {:?}",
25235                            self.peek()
25236                        )));
25237                    }
25238                    self.advance();
25239                    return Ok(Expr::FunctionCall {
25240                        name: String::from("strpos"),
25241                        args: alloc::vec![haystack, needle],
25242                    });
25243                }
25244                // position(sub, str) comma form (incl. bytea) —
25245                // hand the parsed first arg to the generic list.
25246                args.push(needle);
25247                if matches!(self.peek(), Token::Comma) {
25248                    self.advance();
25249                }
25250            }
25251            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25252            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25253            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25254            // riding the generic argument list below.
25255            if first.eq_ignore_ascii_case("trim") {
25256                let mode = match self.peek() {
25257                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25258                        self.advance();
25259                        Some("btrim")
25260                    }
25261                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25262                        self.advance();
25263                        Some("ltrim")
25264                    }
25265                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25266                        self.advance();
25267                        Some("rtrim")
25268                    }
25269                    _ => None,
25270                };
25271                if mode.is_some() || matches!(self.peek(), Token::From) {
25272                    // TRIM([mode] FROM str) — no strip-chars.
25273                    let chars = if matches!(self.peek(), Token::From) {
25274                        None
25275                    } else {
25276                        Some(self.parse_expr(0)?)
25277                    };
25278                    if !matches!(self.peek(), Token::From) {
25279                        return Err(self.err(format!(
25280                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25281                            self.peek()
25282                        )));
25283                    }
25284                    self.advance();
25285                    let target = self.parse_expr(0)?;
25286                    if !matches!(self.peek(), Token::RParen) {
25287                        return Err(
25288                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25289                        );
25290                    }
25291                    self.advance();
25292                    let mut trim_args = alloc::vec![target];
25293                    if let Some(c) = chars {
25294                        trim_args.push(c);
25295                    }
25296                    return Ok(Expr::FunctionCall {
25297                        name: String::from(mode.unwrap_or("btrim")),
25298                        args: trim_args,
25299                    });
25300                }
25301            }
25302            if !matches!(self.peek(), Token::RParen) {
25303                loop {
25304                    // v7.38 (read01, T14) — `argname => value` names this arg.
25305                    // v7.39 (read01 round 77) — `argname := value` is the same
25306                    // thing, and it is the spelling PG's own docs lead with. It
25307                    // was simply never lexed here, so every `f(x := 1)` died in
25308                    // the parser regardless of what `f` was.
25309                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25310                        (
25311                            Token::Ident(n) | Token::QuotedIdent(n),
25312                            Some(Token::FatArrow | Token::ColonEq),
25313                        ) => {
25314                            let name = n.clone();
25315                            self.advance(); // name
25316                            self.advance(); // => / :=
25317                            Some(name)
25318                        }
25319                        _ => None,
25320                    };
25321                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25322                    // array's elements into a variadic call's trailing args
25323                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25324                    // reserved, so it arrives as a bare ident before the arg.
25325                    let is_variadic = this_name.is_none()
25326                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25327                    if is_variadic {
25328                        self.advance();
25329                    }
25330                    let arg = self.parse_expr(0)?;
25331                    args.push(match &this_name {
25332                        // The callee's parameter names decide the slot, and a
25333                        // user function's live in the catalog. Carry the name
25334                        // to eval rather than guessing here.
25335                        Some(n) => Expr::NamedArg {
25336                            name: n.clone(),
25337                            expr: Box::new(arg),
25338                        },
25339                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25340                        None => arg,
25341                    });
25342                    arg_names.push(this_name);
25343                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25344                    // The `::` cast already worked; this lowers the
25345                    // function form onto the same Expr::Cast node.
25346                    if first.eq_ignore_ascii_case("cast")
25347                        && args.len() == 1
25348                        && matches!(self.peek(), Token::As)
25349                    {
25350                        self.advance();
25351                        let target = self.parse_cast_target()?;
25352                        if !matches!(self.peek(), Token::RParen) {
25353                            return Err(self.err(format!(
25354                                "expected ')' to close CAST, got {:?}",
25355                                self.peek()
25356                            )));
25357                        }
25358                        self.advance();
25359                        return Ok(Expr::Cast {
25360                            expr: Box::new(args.pop().expect("one arg")),
25361                            target,
25362                        });
25363                    }
25364                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25365                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25366                    // keywords; SPG's lexer makes them plain idents (so they'd be
25367                    // read as column refs). Lower the keyword to the string form
25368                    // the evaluator already accepts.
25369                    if first.eq_ignore_ascii_case("normalize")
25370                        && args.len() == 1
25371                        && matches!(self.peek(), Token::Comma)
25372                    {
25373                        let form = match self.tokens.get(self.pos + 1) {
25374                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25375                                let up = f.to_ascii_uppercase();
25376                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25377                            }
25378                            _ => None,
25379                        };
25380                        if let Some(up) = form {
25381                            self.advance(); // comma
25382                            self.advance(); // form keyword
25383                            args.push(Expr::Literal(Literal::String(up)));
25384                        }
25385                    }
25386                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25387                    // form. Desugars to the comma-list shape evaluator already
25388                    // handles. Triggered after the first arg when the function
25389                    // name is substring / substr and the next token is FROM
25390                    // (a reserved keyword in PG; SPG also reserves it).
25391                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25392                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25393                    // internal __substring_similar(str, pat, esc) call.
25394                    if (first.eq_ignore_ascii_case("substring")
25395                        || first.eq_ignore_ascii_case("substr"))
25396                        && args.len() == 1
25397                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25398                    {
25399                        self.advance(); // SIMILAR
25400                        let pattern = self.parse_expr(0)?;
25401                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25402                        {
25403                            return Err(self.err(format!(
25404                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25405                                self.peek()
25406                            )));
25407                        }
25408                        self.advance(); // ESCAPE
25409                        let esc = self.parse_expr(0)?;
25410                        if !matches!(self.peek(), Token::RParen) {
25411                            return Err(self.err(format!(
25412                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25413                                self.peek()
25414                            )));
25415                        }
25416                        self.advance();
25417                        args.push(pattern);
25418                        args.push(esc);
25419                        return Ok(Expr::FunctionCall {
25420                            name: "__substring_similar".to_string(),
25421                            args,
25422                        });
25423                    }
25424                    if (first.eq_ignore_ascii_case("substring")
25425                        || first.eq_ignore_ascii_case("substr"))
25426                        && args.len() == 1
25427                        && matches!(self.peek(), Token::From | Token::For)
25428                    {
25429                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25430                        // `substring(str FOR len)` which PG treats as FROM 1.
25431                        if matches!(self.peek(), Token::From) {
25432                            self.advance();
25433                            let start = self.parse_expr(0)?;
25434                            args.push(start);
25435                        } else {
25436                            args.push(Expr::Literal(Literal::Integer(1)));
25437                        }
25438                        if matches!(self.peek(), Token::For) {
25439                            self.advance();
25440                            let length = self.parse_expr(0)?;
25441                            args.push(length);
25442                        }
25443                        if !matches!(self.peek(), Token::RParen) {
25444                            return Err(self.err(format!(
25445                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25446                                self.peek()
25447                            )));
25448                        }
25449                        self.advance();
25450                        return Ok(Expr::FunctionCall {
25451                            name: first.to_ascii_lowercase(),
25452                            args,
25453                        });
25454                    }
25455                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25456                    // syntactic form. Desugars to the `overlay(str,
25457                    // repl, n[, len])` comma-list shape the evaluator
25458                    // already implements. `PLACING` is not a reserved
25459                    // token in SPG, so it arrives as a bare Ident.
25460                    if first.eq_ignore_ascii_case("overlay")
25461                        && args.len() == 1
25462                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25463                    {
25464                        self.advance(); // consume PLACING
25465                        args.push(self.parse_expr(0)?); // replacement
25466                        if !matches!(self.peek(), Token::From) {
25467                            return Err(self.err(format!(
25468                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25469                                self.peek()
25470                            )));
25471                        }
25472                        self.advance();
25473                        args.push(self.parse_expr(0)?); // start position
25474                        if matches!(self.peek(), Token::For) {
25475                            self.advance();
25476                            args.push(self.parse_expr(0)?); // length
25477                        }
25478                        if !matches!(self.peek(), Token::RParen) {
25479                            return Err(self.err(format!(
25480                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25481                                self.peek()
25482                            )));
25483                        }
25484                        self.advance();
25485                        return Ok(Expr::FunctionCall {
25486                            name: String::from("overlay"),
25487                            args,
25488                        });
25489                    }
25490                    // `TRIM(chars FROM str)` — the keyword-less
25491                    // spelling lands here after the chars parse
25492                    // (the keyword forms return earlier).
25493                    if first.eq_ignore_ascii_case("trim")
25494                        && args.len() == 1
25495                        && matches!(self.peek(), Token::From)
25496                    {
25497                        self.advance();
25498                        let target = self.parse_expr(0)?;
25499                        if !matches!(self.peek(), Token::RParen) {
25500                            return Err(self.err(format!(
25501                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25502                                self.peek()
25503                            )));
25504                        }
25505                        self.advance();
25506                        let chars = args.pop().expect("one arg");
25507                        return Ok(Expr::FunctionCall {
25508                            name: String::from("btrim"),
25509                            args: alloc::vec![target, chars],
25510                        });
25511                    }
25512                    // v7.24 (round-16 A) — aggregate-internal
25513                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25514                    // LAST)`. Keys close the argument list.
25515                    if matches!(self.peek(), Token::Order) {
25516                        self.advance();
25517                        if !self.peek_is_by() {
25518                            return Err(self.err(format!(
25519                                "expected BY after ORDER in aggregate args, got {:?}",
25520                                self.peek()
25521                            )));
25522                        }
25523                        self.advance();
25524                        loop {
25525                            // v7.39 (round 691) — save/restore, the discipline this parser
25526                            // already uses around `pending_sample_preds`, so a subquery inside
25527                            // a key neither inherits nor leaks the channel.
25528                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25529                            let saved_coll = self.order_key_collation.take();
25530                            let parsed = self.parse_expr(0);
25531                            self.in_order_by_key = saved_flag;
25532                            let collation =
25533                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25534                            let expr = parsed?;
25535                            let desc = if matches!(self.peek(), Token::Desc) {
25536                                self.advance();
25537                                true
25538                            } else if matches!(self.peek(), Token::Asc) {
25539                                self.advance();
25540                                false
25541                            } else {
25542                                false
25543                            };
25544                            let nulls_first = self.parse_optional_nulls_placement()?;
25545                            agg_order_by.push(OrderBy {
25546                                expr,
25547                                desc,
25548                                nulls_first,
25549                                collation,
25550                            });
25551                            if matches!(self.peek(), Token::Comma) {
25552                                self.advance();
25553                            } else {
25554                                break;
25555                            }
25556                        }
25557                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25558                        // follow the ORDER BY inside GROUP_CONCAT.
25559                        if self.consume_group_concat_separator(&mut args)? {
25560                            saw_separator = true;
25561                        }
25562                        if !matches!(self.peek(), Token::RParen) {
25563                            return Err(self.err(format!(
25564                                "expected ')' after aggregate ORDER BY, got {:?}",
25565                                self.peek()
25566                            )));
25567                        }
25568                        break;
25569                    }
25570                    // v7.39 (round 354, M12) — …or directly after the
25571                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25572                    // own spelling of what PG passes as string_agg's second
25573                    // argument; it was a parse error, so every MySQL query
25574                    // that names its own separator failed outright.
25575                    if self.consume_group_concat_separator(&mut args)? {
25576                        saw_separator = true;
25577                        break;
25578                    }
25579                    match self.peek() {
25580                        Token::Comma => {
25581                            self.advance();
25582                        }
25583                        Token::RParen => break,
25584                        other => {
25585                            return Err(self.err(format!(
25586                                "expected ',' or ')' in function args, got {other:?}"
25587                            )));
25588                        }
25589                    }
25590                }
25591            }
25592            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25593            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25594            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25595            // meaning a separator — that is what the explicit SEPARATOR
25596            // tail is for. Fold them into one `concat(...)` so the
25597            // aggregate keeps its single value argument.
25598            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25599                let values = args.len() - usize::from(saw_separator);
25600                if values > 1 {
25601                    let sep_arg = if saw_separator { args.pop() } else { None };
25602                    let folded = Expr::FunctionCall {
25603                        name: "concat".to_string(),
25604                        args: core::mem::take(&mut args),
25605                    };
25606                    args.push(folded);
25607                    if let Some(sep) = sep_arg {
25608                        args.push(sep);
25609                    }
25610                }
25611            }
25612            self.advance(); // consume ')'
25613            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25614            // any more. The parser has no catalog, so it could only ever resolve
25615            // the handful of `make_*` builtins whose parameter names were baked
25616            // into a table right here — every user function got
25617            // "does not support named arguments", though the catalog has been
25618            // storing its parameter names all along. Reordering happens in eval,
25619            // in one place, for builtins and user functions alike.
25620            // v7.32 (round-29) — ordered-set aggregate tail
25621            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25622            // (percentile_cont / percentile_disc / mode). The sort spec
25623            // lands in the same `order_by` slot a decorated aggregate
25624            // uses; the executor dispatches on the function name. WITHIN
25625            // GROUP and an intra-argument ORDER BY are mutually
25626            // exclusive (PG rejects both).
25627            let within_group_order = self.parse_within_group_clause()?;
25628            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25629                return Err(self.err(
25630                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25631                        .into(),
25632                ));
25633            }
25634            let within_group_seen = !within_group_order.is_empty();
25635            let agg_order_by = if within_group_order.is_empty() {
25636                agg_order_by
25637            } else {
25638                within_group_order
25639            };
25640            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25641            let filter = self.parse_filter_clause()?;
25642            // v4.12: window-function tail — `name(args) OVER (...)`.
25643            // Promotes the just-parsed FunctionCall into a
25644            // WindowFunction node carrying partition + order.
25645            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25646            // / `RESPECT NULLS OVER (...)` between the closing paren
25647            // and `OVER`.
25648            let null_treatment = self.parse_null_treatment_modifier();
25649            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25650                && s.eq_ignore_ascii_case("over")
25651            {
25652                self.advance();
25653                // v7.39 (round 230) — PG implements neither modifier for a
25654                // windowed call and says so (0A000). Both used to be parsed
25655                // and then silently dropped here, so `count(DISTINCT v)
25656                // OVER (…)` quietly answered the non-distinct count.
25657                if agg_distinct {
25658                    return Err(
25659                        self.err("DISTINCT is not implemented for window functions".to_string())
25660                    );
25661                }
25662                if !agg_order_by.is_empty() {
25663                    // PG separates the two shapes that land here: a
25664                    // WITHIN GROUP call is an ordered-set aggregate and gets
25665                    // its own message naming the aggregate; a plain
25666                    // `agg(x ORDER BY y)` gets the generic one.
25667                    let msg = if within_group_seen {
25668                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25669                    } else {
25670                        "aggregate ORDER BY is not implemented for window functions".to_string()
25671                    };
25672                    return Err(self.err(msg));
25673                }
25674                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25675                return Ok(Expr::WindowFunction {
25676                    name: first,
25677                    args,
25678                    partition_by,
25679                    order_by,
25680                    frame,
25681                    null_treatment,
25682                    filter,
25683                });
25684            }
25685            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25686                return Ok(Expr::AggregateOrdered {
25687                    call: Box::new(Expr::FunctionCall { name: first, args }),
25688                    order_by: agg_order_by,
25689                    distinct: agg_distinct,
25690                    filter,
25691                });
25692            }
25693            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25694            // over TIMESTAMPTZ and has no timestamp overload, so a
25695            // timestamp argument is coerced on the way in and the answer
25696            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25697            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25698            // zone`. SPG answered `timestamp without time zone`, dropping
25699            // the offset from every rendering.
25700            //
25701            // Writing the coercion PG performs makes the existing
25702            // argument-driven typing (the one `date_trunc` uses) reach the
25703            // right answer, rather than teaching the type layer a second
25704            // rule. MySQL's DATE_ADD is a different function that returns
25705            // DATE or DATETIME, so this is PG-dialect only.
25706            //
25707            // Out-of-line because this sits on the RECURSIVE descent
25708            // frame: an inline block with locals here costs every nesting
25709            // level, and the suite's deep-nesting sentinel overflowed the
25710            // 512 KiB parser stack the moment one was added (round 430's
25711            // lesson, in the same shape).
25712            if !self.mysql_dialect {
25713                lift_date_add_arg_to_timestamptz(&first, &mut args);
25714            }
25715            return Ok(Expr::FunctionCall { name: first, args });
25716        }
25717        // v7.9.20 — SQL-standard parenless keyword expressions
25718        // (PG treats these as functions called without parens).
25719        // Resolve to a synthetic FunctionCall so the engine's
25720        // eval path reuses the existing function-call routing.
25721        // mailrs G3.
25722        let lc = first.to_ascii_lowercase();
25723        if matches!(
25724            lc.as_str(),
25725            "current_date"
25726                | "current_time"
25727                | "current_timestamp"
25728                | "localtimestamp"
25729                | "localtime"
25730                // v7.37.17 (17.6 siblings) — session-identity SQL-
25731                // standard parenless keywords. current_user /
25732                // session_user / user were already caught by the
25733                // pgwire canned-response shortcut but bare-select
25734                // in the embedded engine went through Expr::Column
25735                // and errored. Adding them here so the parser
25736                // resolves to a synthetic FunctionCall that reuses
25737                // the existing eval/functions.rs dispatch.
25738                | "current_user"
25739                | "session_user"
25740                | "current_role"
25741                | "current_catalog"
25742                | "current_schema"
25743                | "current_database"
25744                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25745                | "system_user"
25746        ) {
25747            return Ok(Expr::FunctionCall {
25748                name: lc,
25749                args: Vec::new(),
25750            });
25751        }
25752        Ok(Expr::Column(ColumnName {
25753            qualifier: None,
25754            name: first,
25755        }))
25756    }
25757}
25758
25759/// v7.39 (round 522) — write the coercion PG's `date_add` /
25760/// `date_subtract` signature performs.
25761///
25762/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25763/// timestamp argument is cast on the way in and the answer is
25764/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25765/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25766/// `timestamp without time zone`, dropping the offset from every
25767/// rendering of the result.
25768///
25769/// Writing the cast the signature implies lets the existing
25770/// argument-driven typing (the one `date_trunc` uses) reach the right
25771/// answer instead of teaching the type layer a second rule. MySQL's
25772/// DATE_ADD is a different function returning DATE or DATETIME, so the
25773/// caller applies this in PG dialect only.
25774///
25775/// A free function, and not a block at the call site, because the caller
25776/// is on the recursive-descent frame chain.
25777#[inline(never)]
25778fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25779    if args.len() != 2
25780        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25781    {
25782        return;
25783    }
25784    let base = args.remove(0);
25785    args.insert(
25786        0,
25787        Expr::Cast {
25788            expr: Box::new(base),
25789            target: CastTarget::Timestamptz,
25790        },
25791    );
25792}
25793
25794/// v6.8.2 — walk an expression tree and return the first column
25795/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25796/// to derive `CreateIndexStatement.column` from an expression
25797/// key (so downstream planner code resolving a primary column
25798/// position keeps working with expression indexes). Returns
25799/// `None` when the expression has no column ref at all — caller
25800/// surfaces that as a parse error.
25801fn extract_first_column(expr: &Expr) -> Option<String> {
25802    match expr {
25803        Expr::Column(cn) => Some(cn.name.clone()),
25804        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25805        Expr::Binary { lhs, rhs, .. } => {
25806            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25807        }
25808        Expr::Unary { expr: e, .. } => extract_first_column(e),
25809        // v7.39 (read01 round 93) — a cast wraps its operand: a common
25810        // expression-index key is `lower(col::text)`, where the column
25811        // sits under the `::text` cast inside the function arg. Without
25812        // descending here the key was rejected as "references no column".
25813        Expr::Cast { expr: e, .. } => extract_first_column(e),
25814        _ => None,
25815    }
25816}
25817
25818fn maybe_not(expr: Expr, negated: bool) -> Expr {
25819    if negated {
25820        Expr::Unary {
25821            op: UnOp::Not,
25822            expr: Box::new(expr),
25823        }
25824    } else {
25825        expr
25826    }
25827}
25828
25829/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25830/// things in the two dialects, and SPG read all three PG's way:
25831///
25832/// | token | PG (and SPG) | MySQL, measured |
25833/// |---|---|---|
25834/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25835/// | `&&` | inet / array overlap | **AND** |
25836/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25837///
25838/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25839/// answer with no error, which is why they are routed here rather than
25840/// left to the shared table.
25841impl Parser {
25842    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25843        if self.mysql_dialect {
25844            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25845            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25846            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25847            if let Token::Ident(w) = tok
25848                && w.eq_ignore_ascii_case("div")
25849            {
25850                return Some((BinOp::IntDiv, 8));
25851            }
25852            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25853            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25854            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25855            // there sits in operand position, not infix).
25856            if let Token::Ident(w) = tok
25857                && w.eq_ignore_ascii_case("mod")
25858            {
25859                return Some((BinOp::Mod, 8));
25860            }
25861            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25862            // plain ident to the lexer. Its precedence sits between OR (1)
25863            // and AND (3) — hence rung 2, the slot freed by moving AND up.
25864            if let Token::Ident(w) = tok
25865                && w.eq_ignore_ascii_case("xor")
25866            {
25867                return Some((BinOp::LogicalXor, 2));
25868            }
25869            match tok {
25870                Token::Concat => return Some((BinOp::Or, 1)),
25871                // MySQL's `&&` is logical AND, sharing AND's rung (3).
25872                Token::InetOverlap => return Some((BinOp::And, 3)),
25873                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25874                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25875                _ => {}
25876            }
25877        }
25878        binop_from(tok)
25879    }
25880}
25881
25882// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25883// (which sits strictly between OR and AND), every level from AND upward was
25884// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25885// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25886// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25887// the *relative* order of every PG operator is unchanged by the shift.
25888fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25889    let pair = match tok {
25890        Token::Or => (BinOp::Or, 1),
25891        Token::And => (BinOp::And, 3),
25892        Token::Eq => (BinOp::Eq, 5),
25893        Token::NotEq => (BinOp::NotEq, 5),
25894        Token::Lt => (BinOp::Lt, 5),
25895        Token::LtEq => (BinOp::LtEq, 5),
25896        Token::Gt => (BinOp::Gt, 5),
25897        Token::GtEq => (BinOp::GtEq, 5),
25898        // pgvector distance ops all sit on the same rung — tighter than
25899        // comparisons (5) so `col <-> v < threshold` parses correctly.
25900        Token::L2Distance => (BinOp::L2Distance, 6),
25901        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25902        // comparison rung.
25903        Token::GeomParallel => (BinOp::GeomParallel, 5),
25904        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25905        // comparison rung.
25906        Token::OverLeft => (BinOp::OverLeft, 5),
25907        Token::OverRight => (BinOp::OverRight, 5),
25908        Token::GeomPerp => (BinOp::GeomPerp, 5),
25909        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25910        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25911        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25912        Token::InnerProduct => (BinOp::InnerProduct, 6),
25913        Token::CosineDistance => (BinOp::CosineDistance, 6),
25914        Token::Plus => (BinOp::Add, 7),
25915        Token::Minus => (BinOp::Sub, 7),
25916        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25917        // binds every "other" operator (`||`, `|`, `&`, `#`, the
25918        // pgvector distances above) BETWEEN additive (7) and the
25919        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25920        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25921        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25922        // ("matches PG conceptually" — the round-753 audit measured it
25923        // false; the old rung errored on `'a' || 1 + 1` with
25924        // `text + integer`). Same-level chains left-fold, as PG does.
25925        Token::Concat => (BinOp::Concat, 6),
25926        Token::Pipe => (BinOp::BitOr, 6),
25927        Token::Amp => (BinOp::BitAnd, 6),
25928        Token::Star => (BinOp::Mul, 8),
25929        Token::Slash => (BinOp::Div, 8),
25930        Token::Percent => (BinOp::Mod, 8),
25931        // v4.14: JSON path ops bind tighter than comparisons (5)
25932        // and additive (7) so `doc->'k' = 'v'` parses correctly.
25933        // Same rung as the multiplicative ops.
25934        Token::JsonGet => (BinOp::JsonGet, 8),
25935        Token::JsonGetText => (BinOp::JsonGetText, 8),
25936        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25937        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25938        Token::JsonContains => (BinOp::JsonContains, 8),
25939        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25940        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25941        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25942        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25943        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25944        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25945        // v7.12.2 — `@@` binds at the comparison rung (looser than
25946        // arithmetic, tighter than AND / OR). PG places `@@` at
25947        // the same precedence as `=` / `<`, so we follow.
25948        Token::TsMatch => (BinOp::TsMatch, 5),
25949        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25950        // PG places these at the comparison rung (same level as `=`),
25951        // so we follow.
25952        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25953        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25954        Token::InetContains => (BinOp::InetContains, 5),
25955        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25956        Token::InetOverlap => (BinOp::InetOverlap, 5),
25957        // v7.39 (round 508) — the geometric and pattern-order predicates
25958        // ride the comparison rung, as every other predicate does.
25959        Token::Intersects => (BinOp::Intersects, 5),
25960        Token::IsBelow => (BinOp::IsBelow, 5),
25961        Token::IsAbove => (BinOp::IsAbove, 5),
25962        Token::PatternLt => (BinOp::PatternLt, 5),
25963        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25964        Token::PatternGt => (BinOp::PatternGt, 5),
25965        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25966        // `@@@` is the old spelling of `@@` and means exactly it.
25967        Token::TsMatchOld => (BinOp::TsMatch, 5),
25968        _ => return None,
25969    };
25970    Some(pair)
25971}
25972
25973#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25974// `as f32` here is intentional: vector elements widen / narrow into f32 on
25975// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25976// past ~15 decimal digits — both are acceptable for a fixed-precision
25977// pgvector column.
25978/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25979/// implicit table alias and break trailing clauses. WITH lands
25980/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25981/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25982/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25983/// / VALUES / FOR / LATERAL — all of which would otherwise be
25984/// silently swallowed by `parse_optional_alias`.
25985fn is_alias_stopword(s: &str) -> bool {
25986    matches!(
25987        s.to_ascii_lowercase().as_str(),
25988        "with"
25989            | "on"
25990            | "where"
25991            | "having"
25992            | "group"
25993            | "order"
25994            | "limit"
25995            | "offset"
25996            | "union"
25997            | "except"
25998            | "intersect"
25999            | "returning"
26000            | "set"
26001            | "values"
26002            | "for"
26003            | "window"
26004            | "tablesample"
26005            | "lateral"
26006            | "left"
26007            | "right"
26008            | "inner"
26009            | "outer"
26010            | "full"
26011            | "cross"
26012            | "join"
26013            | "natural"
26014            | "using"
26015            | "fetch"
26016    )
26017}
26018
26019fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26020    match e {
26021        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26022        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26023        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26024        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26025        // so scale the divisor by hand instead of `f32::powi`.)
26026        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26027            let mut div = 1.0f32;
26028            for _ in 0..*scale {
26029                div *= 10.0;
26030            }
26031            Some(*unscaled as f32 / div)
26032        }
26033        Expr::Unary {
26034            op: UnOp::Neg,
26035            expr,
26036        } => extract_numeric_literal(expr).map(|x| -x),
26037        _ => None,
26038    }
26039}
26040
26041/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26042/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26043/// negative. Returns `None` if any pair fails to parse or no pair is found.
26044///
26045/// Recognised units (case-insensitive, optional trailing `s`):
26046/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26047/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26048/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26049/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26050/// (PG-canonical: DST and month-boundary semantics depend on this).
26051/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26052/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26053/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26054#[allow(clippy::cast_possible_truncation)]
26055fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26056    let mut months: i64 = 0;
26057    let mut days: i64 = 0;
26058    let mut micros: i64 = 0;
26059    let mut in_time = false;
26060    let mut num = alloc::string::String::new();
26061    for ch in rest.chars() {
26062        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26063            num.push(ch);
26064            continue;
26065        }
26066        if ch == 'T' || ch == 't' {
26067            if !num.is_empty() {
26068                return None;
26069            }
26070            in_time = true;
26071            continue;
26072        }
26073        let n: f64 = num.parse().ok()?;
26074        num.clear();
26075        match (ch, in_time) {
26076            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26077            ('M', false) => months += n as i64,
26078            ('W' | 'w', false) => days += (n * 7.0) as i64,
26079            ('D' | 'd', false) => days += n as i64,
26080            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26081            ('M', true) => micros += (n * 60_000_000.0) as i64,
26082            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26083            _ => return None,
26084        }
26085    }
26086    if !num.is_empty() {
26087        return None;
26088    }
26089    Some((
26090        i32::try_from(months).ok()?,
26091        i32::try_from(days).ok()?,
26092        micros,
26093    ))
26094}
26095
26096/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26097/// leading `-` negates the whole value). Rejects date-like strings.
26098fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26099    let (neg, body) = match s.strip_prefix('-') {
26100        Some(b) => (true, b),
26101        None => (false, s),
26102    };
26103    let (y, m) = body.split_once('-')?;
26104    let years: i32 = y.parse().ok()?;
26105    let mons: i32 = m.parse().ok()?;
26106    if years < 0 || mons < 0 {
26107        return None;
26108    }
26109    let total = years.checked_mul(12)?.checked_add(mons)?;
26110    Some((if neg { -total } else { total }, 0, 0))
26111}
26112
26113/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26114/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26115fn parse_interval_clock(tok: &str) -> Option<i64> {
26116    let (neg, body) = match tok.strip_prefix('-') {
26117        Some(r) => (true, r),
26118        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26119    };
26120    let mut it = body.split(':');
26121    let h: i64 = it.next()?.parse().ok()?;
26122    let m: i64 = it.next()?.parse().ok()?;
26123    let s_tok = it.next().unwrap_or("0");
26124    if it.next().is_some() {
26125        return None;
26126    }
26127    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26128        let sec: i64 = sec.parse().ok()?;
26129        let mut f = alloc::string::String::from(frac);
26130        while f.len() < 6 {
26131            f.push('0');
26132        }
26133        f.truncate(6);
26134        let fus: i64 = f.parse().ok()?;
26135        sec.checked_mul(1_000_000)?.checked_add(fus)?
26136    } else {
26137        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26138    };
26139    let total = h
26140        .checked_mul(3_600_000_000)?
26141        .checked_add(m.checked_mul(60_000_000)?)?
26142        .checked_add(sec_us)?;
26143    Some(if neg { -total } else { total })
26144}
26145
26146/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26147/// every spelling PG accepts (measured against live PG18.4, not guessed):
26148/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26149/// Before this, the unit table matched long names only, with an ad-hoc
26150/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26151/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26152/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26153/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26154/// fractional) both read from this one table now.
26155fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26156    let u = raw.to_ascii_lowercase();
26157    Some(match u.as_str() {
26158        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26159            "microsecond"
26160        }
26161        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26162            "millisecond"
26163        }
26164        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26165        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26166        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26167        "day" | "days" | "d" => "day",
26168        "week" | "weeks" | "w" => "week",
26169        "month" | "months" | "mon" | "mons" => "month",
26170        "year" | "years" | "yr" | "yrs" | "y" => "year",
26171        "decade" | "decades" | "dec" | "decs" => "decade",
26172        "century" | "centuries" | "cent" | "c" => "century",
26173        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26174        _ => return None,
26175    })
26176}
26177
26178/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26179/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26181pub(crate) enum IntervalField {
26182    Year,
26183    Month,
26184    Day,
26185    Hour,
26186    Minute,
26187    Second,
26188}
26189
26190/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26191/// spellings aren't standard for the qualifier position, so only the singular
26192/// forms are accepted.
26193/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26194/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26195/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26196/// take a `'1 2'` style literal — are not read here; they stay a parse
26197/// error rather than being silently misread.)
26198/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26199///
26200/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26201/// to do with a `@@` engine setting, and an unset one reads NULL rather
26202/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26203/// were the same node and `SELECT @x` answered "Unknown system variable".)
26204/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26205/// not see a session override — measured, after `SET autocommit=0`,
26206/// `@@global.autocommit` is still 1.
26207///
26208/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26209/// the parser's nesting budget is tuned against, and building these
26210/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26211/// wall `parse_left_right_atom` and friends were factored out for).
26212#[inline(never)]
26213fn variable_ref_atom(raw: &str) -> Expr {
26214    let user_var = !raw.starts_with("@@");
26215    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26216    Expr::FunctionCall {
26217        name: String::from(if user_var {
26218            "__spg_user_var"
26219        } else {
26220            "__spg_session_var"
26221        }),
26222        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26223    }
26224}
26225
26226fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26227    let Token::Ident(s) = tok else { return None };
26228    Some(match () {
26229        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26230        () if s.eq_ignore_ascii_case("second") => "second",
26231        () if s.eq_ignore_ascii_case("minute") => "minute",
26232        () if s.eq_ignore_ascii_case("hour") => "hour",
26233        () if s.eq_ignore_ascii_case("day") => "day",
26234        () if s.eq_ignore_ascii_case("week") => "week",
26235        () if s.eq_ignore_ascii_case("month") => "month",
26236        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26237        () if s.eq_ignore_ascii_case("year") => "year",
26238        () => return None,
26239    })
26240}
26241
26242/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26243/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26244/// which constructs the value at run time. Only the slot the unit names
26245/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26246/// slot the builtin has (months and fractional seconds respectively).
26247fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26248    let zero = || Expr::Literal(Literal::Integer(0));
26249    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26250        lhs: alloc::boxed::Box::new(qty.clone()),
26251        op,
26252        rhs: alloc::boxed::Box::new(by),
26253    };
26254    // (years, months, weeks, days, hours, mins, secs)
26255    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26256    match unit {
26257        "year" => args[0] = qty,
26258        "quarter" => {
26259            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26260        }
26261        "month" => args[1] = qty,
26262        "week" => args[2] = qty,
26263        "day" => args[3] = qty,
26264        "hour" => args[4] = qty,
26265        "minute" => args[5] = qty,
26266        "second" => args[6] = qty,
26267        // The builtin's seconds slot takes a fraction, so microseconds ride
26268        // it scaled down; the divisor is a NUMERIC literal so the division
26269        // stays exact rather than going through a float.
26270        "microsecond" => {
26271            args[6] = scaled(
26272                crate::ast::BinOp::Div,
26273                Expr::Literal(Literal::Numeric {
26274                    unscaled: 1_000_000,
26275                    scale: 0,
26276                }),
26277            );
26278        }
26279        _ => args[3] = qty,
26280    }
26281    Expr::FunctionCall {
26282        name: alloc::string::String::from("make_interval"),
26283        args,
26284    }
26285}
26286
26287/// `(count, unit)` → `(months, days, micros)`.
26288fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26289    let n: i64 = count.trim().parse().ok()?;
26290    Some(match unit {
26291        "microsecond" => (0, 0, n),
26292        "second" => (0, 0, n.checked_mul(1_000_000)?),
26293        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26294        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26295        "day" => (0, i32::try_from(n).ok()?, 0),
26296        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26297        "month" => (i32::try_from(n).ok()?, 0, 0),
26298        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26299        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26300        _ => return None,
26301    })
26302}
26303
26304fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26305    let Token::Ident(s) = tok else { return None };
26306    Some(match () {
26307        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26308        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26309        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26310        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26311        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26312        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26313        () => return None,
26314    })
26315}
26316
26317/// v7.39 (read01 round 102) — interpret an interval literal under a field
26318/// qualifier. Returns `(months, days, micros)`.
26319///
26320/// * A single field applied to a bare number sets which unit the number means,
26321///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26322///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26323/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26324/// * Every other range, and any literal a single field can't read as a plain
26325///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26326///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26327///   like PG, and the qualifier there only bounds precision.
26328fn interpret_qualified_interval(
26329    text: &str,
26330    (f1, f2): (IntervalField, Option<IntervalField>),
26331) -> Option<(i32, i32, i64)> {
26332    if let Some(f2) = f2 {
26333        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26334            if let Some(m) = parse_year_month_literal(text) {
26335                return Some((m, 0, 0));
26336            }
26337        }
26338        return parse_interval_text(text);
26339    }
26340    // Single field: reinterpret a bare number; otherwise the default parse.
26341    let trimmed = text.trim();
26342    if let Ok(val) = trimmed.parse::<f64>() {
26343        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26344        #[allow(clippy::cast_possible_truncation)]
26345        let whole = val as i64;
26346        #[allow(clippy::cast_possible_truncation)]
26347        let secs_micros = {
26348            let m = val * 1_000_000.0;
26349            if m >= 0.0 {
26350                (m + 0.5) as i64
26351            } else {
26352                (m - 0.5) as i64
26353            }
26354        };
26355        return Some(match f1 {
26356            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26357            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26358            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26359            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26360            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26361            IntervalField::Second => (0, 0, secs_micros),
26362        });
26363    }
26364    parse_interval_text(text)
26365}
26366
26367/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26368fn parse_year_month_literal(text: &str) -> Option<i32> {
26369    let t = text.trim();
26370    let (neg, body) = match t.strip_prefix('-') {
26371        Some(r) => (true, r),
26372        None => (false, t.strip_prefix('+').unwrap_or(t)),
26373    };
26374    let mut it = body.split('-');
26375    let years: i32 = it.next()?.trim().parse().ok()?;
26376    let months: i32 = match it.next() {
26377        Some(m) => m.trim().parse().ok()?,
26378        None => 0,
26379    };
26380    if it.next().is_some() {
26381        return None;
26382    }
26383    let total = years.checked_mul(12)?.checked_add(months)?;
26384    Some(if neg { -total } else { total })
26385}
26386
26387pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26388    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26389    // `@` is decorative; a trailing `ago` negates the whole interval.
26390    let mut trimmed = s.trim();
26391    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26392    let mut negate = false;
26393    if let Some(rest) = trimmed
26394        .strip_suffix("ago")
26395        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26396    {
26397        negate = true;
26398        trimmed = rest.trim();
26399    }
26400    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26401        let (mo, d, us) = v?;
26402        if negate {
26403            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26404        } else {
26405            Some((mo, d, us))
26406        }
26407    };
26408    let s = trimmed;
26409    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26410    // are single tokens, not the `<n> <unit>` pair form handled below.
26411    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26412        return finish(parse_iso8601_interval(rest));
26413    }
26414    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26415        if let Some(iv) = parse_year_month_interval(trimmed) {
26416            return finish(Some(iv));
26417        }
26418    }
26419    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26420    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26421    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26422    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26423        if let Ok(n) = trimmed.parse::<i64>() {
26424            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26425        }
26426        if let Ok(f) = trimmed.parse::<f64>() {
26427            if f.is_finite() {
26428                #[allow(clippy::cast_possible_truncation)]
26429                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26430            }
26431        }
26432    }
26433    // v7.39 (round 243) — PG accepts the number and unit run together
26434    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26435    // the `<n> <unit>` pair loop below sees them as two.
26436    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26437    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26438    for p in raw_parts {
26439        let boundary = p
26440            .char_indices()
26441            .find(|(i, c)| {
26442                *i > 0
26443                    && c.is_ascii_alphabetic()
26444                    && p[..*i]
26445                        .chars()
26446                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26447                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26448            })
26449            .map(|(i, _)| i);
26450        match boundary {
26451            Some(i) => {
26452                parts.push(&p[..i]);
26453                parts.push(&p[i..]);
26454            }
26455            None => parts.push(p),
26456        }
26457    }
26458    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26459    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26460    // remains is the `<n> <unit>` pair form handled below.
26461    let mut clock_us: i64 = 0;
26462    let mut had_clock = false;
26463    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26464        clock_us = parse_interval_clock(parts[pos])?;
26465        parts.remove(pos);
26466        had_clock = true;
26467    }
26468    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26469    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26470    let mut lone_days: i32 = 0;
26471    if had_clock && parts.len() == 1 {
26472        if let Ok(n) = parts[0].parse::<i64>() {
26473            lone_days = i32::try_from(n).ok()?;
26474            parts.clear();
26475        }
26476    }
26477    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26478        return None;
26479    }
26480    let mut months: i32 = 0;
26481    let mut days: i32 = lone_days;
26482    let mut micros: i64 = clock_us;
26483    let mut i = 0;
26484    while i < parts.len() {
26485        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26486        if let Ok(n) = parts[i].parse::<i64>() {
26487            match unit_stripped {
26488                "microsecond" => micros = micros.checked_add(n)?,
26489                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26490                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26491                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26492                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26493                "day" => {
26494                    let n32 = i32::try_from(n).ok()?;
26495                    days = days.checked_add(n32)?;
26496                }
26497                "week" => {
26498                    let n32 = i32::try_from(n).ok()?;
26499                    days = days.checked_add(n32.checked_mul(7)?)?;
26500                }
26501                "month" => {
26502                    let n32 = i32::try_from(n).ok()?;
26503                    months = months.checked_add(n32)?;
26504                }
26505                "year" => {
26506                    let n32 = i32::try_from(n).ok()?;
26507                    months = months.checked_add(n32.checked_mul(12)?)?;
26508                }
26509                // v7.39 (read01 timestamp.c) — the larger calendar units.
26510                "decade" => {
26511                    let n32 = i32::try_from(n).ok()?;
26512                    months = months.checked_add(n32.checked_mul(120)?)?;
26513                }
26514                "century" => {
26515                    let n32 = i32::try_from(n).ok()?;
26516                    months = months.checked_add(n32.checked_mul(1200)?)?;
26517                }
26518                "millennium" => {
26519                    let n32 = i32::try_from(n).ok()?;
26520                    months = months.checked_add(n32.checked_mul(12000)?)?;
26521                }
26522                _ => return None,
26523            }
26524        } else if let Ok(f) = parts[i].parse::<f64>() {
26525            // Fractional units cascade down to the next-finer field the way
26526            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26527            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26528            // no_std: f64 has no trunc/fract/round methods, so do them with
26529            // casts (toward-zero) + explicit round-half-away-from-zero.
26530            #[allow(clippy::cast_possible_truncation)]
26531            fn round_i64(x: f64) -> i64 {
26532                if x >= 0.0 {
26533                    (x + 0.5) as i64
26534                } else {
26535                    (x - 0.5) as i64
26536                }
26537            }
26538            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26539            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26540                const DAY_US: f64 = 86_400_000_000.0;
26541                let whole = d as i64; // truncates toward zero
26542                let frac = d - whole as f64;
26543                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26544                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26545                Some(())
26546            }
26547            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26548            match unit_stripped {
26549                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26550                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26551                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26552                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26553                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26554                "day" => add_days_frac(&mut days, &mut micros, f)?,
26555                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26556                "month" => {
26557                    let whole = f as i64;
26558                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26559                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26560                }
26561                "year" => {
26562                    let m = f * 12.0;
26563                    let whole = m as i64;
26564                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26565                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26566                }
26567                _ => return None,
26568            }
26569        } else {
26570            return None;
26571        }
26572        i += 2;
26573    }
26574    finish(Some((months, days, micros)))
26575}
26576
26577/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26578/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26579/// `interval` is intentionally absent (handled by its own parser arm).
26580/// Returns `None` for names that aren't sensible as a bare typed literal, so
26581/// the caller falls back to treating the ident as a column reference.
26582fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26583    Some(match ident {
26584        "date" => CastTarget::Date,
26585        "timestamp" | "datetime" => CastTarget::Timestamp,
26586        "timestamptz" => CastTarget::Timestamptz,
26587        "bool" | "boolean" => CastTarget::Bool,
26588        "int" | "integer" | "int4" => CastTarget::Int,
26589        "bigint" | "int8" => CastTarget::BigInt,
26590        "float8" | "double precision" => CastTarget::Float,
26591        "uuid" => CastTarget::Uuid,
26592        "bytea" => CastTarget::Bytea,
26593        "json" => CastTarget::Json,
26594        "jsonb" => CastTarget::Jsonb,
26595        // Types without a dedicated CastTarget variant flow through the
26596        // generic Named path (engine resolves via column_type_to_data_type).
26597        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26598        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26599        | "money" | "bit" | "varbit"
26600        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26601        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26602        // Range / multirange types likewise.
26603        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26604        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26605        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26606        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26607        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26608            CastTarget::Named(alloc::string::String::from(ident))
26609        }
26610        _ => return None,
26611    })
26612}
26613
26614/// v7.12.4 — map a bare type-name identifier (the form that
26615/// appears in a function arg list or RETURNS clause) to a
26616/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26617/// types so the caller can preserve them as
26618/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26619///
26620/// Subset of the full column-type grammar — we deliberately
26621/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26622/// here because function-arg types in v7.12.4 are mostly the
26623/// bare form (`text`, `int`, `bytea`, …).
26624/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26625/// than being `name TYPE`?
26626///
26627/// The multi-word spellings SQL allows for a bare argument type, each
26628/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26629///
26630/// NOTE this list also exists in `spg-storage`, which computes the
26631/// signature key from the rendered argument text and has to reach the
26632/// same verdict. The two crates are siblings — neither depends on the
26633/// other — and each already carries its own table of type spellings
26634/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26635/// there), so this follows the structure rather than inventing new
26636/// duplication. Recorded as V49.
26637pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26638    let t = phrase.trim().to_ascii_lowercase();
26639    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26640    matches!(
26641        base,
26642        "double precision"
26643            | "character varying"
26644            | "bit varying"
26645            | "timestamp with time zone"
26646            | "timestamp without time zone"
26647            | "time with time zone"
26648            | "time without time zone"
26649            | "national character"
26650            | "national character varying"
26651    )
26652}
26653
26654fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26655    Some(match ident.to_ascii_lowercase().as_str() {
26656        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26657        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26658        "bigint" => ColumnTypeName::BigInt,
26659        "float" | "double" => ColumnTypeName::Float,
26660        // v7.39 (round 269) — real is 32-bit.
26661        "real" | "float4" => ColumnTypeName::Real,
26662        "text" => ColumnTypeName::Text,
26663        "bool" | "boolean" => ColumnTypeName::Bool,
26664        "date" => ColumnTypeName::Date,
26665        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26666        "timestamptz" => ColumnTypeName::Timestamptz,
26667        "json" => ColumnTypeName::Json,
26668        "jsonb" => ColumnTypeName::Jsonb,
26669        "bytea" | "bytes" => ColumnTypeName::Bytes,
26670        "tsvector" => ColumnTypeName::TsVector,
26671        "tsquery" => ColumnTypeName::TsQuery,
26672        "uuid" => ColumnTypeName::Uuid,
26673        "interval" => ColumnTypeName::Interval,
26674        "time" => ColumnTypeName::Time,
26675        "year" => ColumnTypeName::Year,
26676        "timetz" => ColumnTypeName::TimeTz,
26677        "money" => ColumnTypeName::Money,
26678        _ => return None,
26679    })
26680}
26681
26682/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26683/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26684///
26685/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26686/// / embedded SQL land in v7.12.5+):
26687///
26688/// ```text
26689///   body          := [ws] block [ws]
26690///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26691///   stmt          := assign | return
26692///   assign        := assign_target := expr
26693///   assign_target := ( NEW | OLD ) . ident | ident
26694///   return        := RETURN ( NEW | OLD | NULL | expr )
26695/// ```
26696///
26697/// `expr` is parsed by recursing into the regular `Parser` — so a
26698/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26699/// NEW.subject || ' ' || NEW.sender)` body shape works without
26700/// the body parser knowing what `to_tsvector` is.
26701///
26702/// Errors here cause the caller to fall back to
26703/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26704/// successful, but the executor will refuse to invoke the
26705/// function with an "unparseable body" error.
26706/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26707/// from the crate root as `spg_sql::parse_function_body`.
26708pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26709    parse_plpgsql_body(body)
26710}
26711
26712fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26713    // Use the regular lexer on the body text. The trailing
26714    // `END;` may or may not have a semicolon; the lexer treats
26715    // both forms identically.
26716    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26717        message: alloc::format!("plpgsql body lex error: {e}"),
26718        token_pos: 0,
26719    })?;
26720    let mut parser = Parser::new(tokens);
26721    parser.parse_plpgsql_block()
26722}
26723
26724/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26725fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26726    match v {
26727        crate::ast::SetValue::String(s)
26728        | crate::ast::SetValue::Ident(s)
26729        | crate::ast::SetValue::Number(s) => s.clone(),
26730        crate::ast::SetValue::Default => "DEFAULT".into(),
26731    }
26732}
26733
26734/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26735/// contains an aggregate call at ITS OWN query level (recursion stops at
26736/// sublink boundaries — a sublink's aggregates belong to the sublink).
26737/// Backs the "aggregate functions are not allowed in a recursive query's
26738/// recursive term" well-formedness check.
26739fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26740    const AGG_NAMES: &[&str] = &[
26741        "count",
26742        "sum",
26743        "min",
26744        "max",
26745        "avg",
26746        "string_agg",
26747        "array_agg",
26748        "bool_and",
26749        "bool_or",
26750        "every",
26751        "any_value",
26752        "json_agg",
26753        "jsonb_agg",
26754        "json_object_agg",
26755        "jsonb_object_agg",
26756        "bit_and",
26757        "bit_or",
26758        "bit_xor",
26759        "var_pop",
26760        "var_samp",
26761        "variance",
26762        "stddev",
26763        "stddev_pop",
26764        "stddev_samp",
26765        "range_agg",
26766        "range_intersect_agg",
26767        "percentile_cont",
26768        "percentile_disc",
26769        "mode",
26770        "corr",
26771        "covar_pop",
26772        "covar_samp",
26773    ];
26774    match e {
26775        Expr::AggregateOrdered { .. } => true,
26776        Expr::FunctionCall { name, args } => {
26777            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26778                || args.iter().any(expr_has_toplevel_aggregate)
26779        }
26780        Expr::NamedArg { expr, .. }
26781        | Expr::Variadic(expr)
26782        | Expr::Unary { expr, .. }
26783        | Expr::Cast { expr, .. }
26784        | Expr::IsNull { expr, .. }
26785        | Expr::FieldAccess { base: expr, .. }
26786        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26787        Expr::Binary { lhs, rhs, .. } => {
26788            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26789        }
26790        Expr::Like { expr, pattern, .. } => {
26791            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26792        }
26793        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26794        Expr::InList { expr, list, .. } => {
26795            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26796        }
26797        Expr::ArraySubscript { target, index } => {
26798            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26799        }
26800        Expr::ArraySlice { target, lo, hi } => {
26801            expr_has_toplevel_aggregate(target)
26802                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26803                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26804        }
26805        Expr::AnyAll { expr, array, .. } => {
26806            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26807        }
26808        Expr::Case {
26809            operand,
26810            branches,
26811            else_branch,
26812        } => {
26813            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26814                || branches
26815                    .iter()
26816                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26817                || else_branch
26818                    .as_deref()
26819                    .is_some_and(expr_has_toplevel_aggregate)
26820        }
26821        // The outer-level operands of a sublink can aggregate; the sublink's
26822        // own body cannot leak its aggregates up here.
26823        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26824        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26825            row.iter().any(expr_has_toplevel_aggregate)
26826        }
26827        _ => false,
26828    }
26829}
26830
26831/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26832/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26833/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26834/// sublink and is legal in a recursive term, so it is not walked here.
26835fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26836    let mut exprs: Vec<&Expr> = Vec::new();
26837    for it in &s.items {
26838        if let crate::ast::SelectItem::Expr { expr, .. } = it {
26839            exprs.push(expr);
26840        }
26841    }
26842    if let Some(w) = &s.where_ {
26843        exprs.push(w);
26844    }
26845    if let Some(h) = &s.having {
26846        exprs.push(h);
26847    }
26848    if let Some(g) = &s.group_by {
26849        exprs.extend(g.iter());
26850    }
26851    if let Some(from) = &s.from {
26852        for j in &from.joins {
26853            if let Some(on) = &j.on {
26854                exprs.push(on);
26855            }
26856        }
26857    }
26858    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26859}
26860
26861/// Does this expression contain a sublink whose subquery mentions `name`?
26862fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26863    match e {
26864        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26865        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26866        Expr::InSubquery { expr, subquery, .. } => {
26867            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26868        }
26869        Expr::RowInSubquery { row, subquery, .. } => {
26870            row.iter().any(|x| expr_sublink_mentions(x, name))
26871                || select_mentions_table(subquery, name)
26872        }
26873        Expr::RowCmpSubquery { row, subquery, .. } => {
26874            row.iter().any(|x| expr_sublink_mentions(x, name))
26875                || select_mentions_table(subquery, name)
26876        }
26877        Expr::NamedArg { expr, .. }
26878        | Expr::Variadic(expr)
26879        | Expr::Unary { expr, .. }
26880        | Expr::Cast { expr, .. }
26881        | Expr::IsNull { expr, .. }
26882        | Expr::FieldAccess { base: expr, .. }
26883        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26884        Expr::Binary { lhs, rhs, .. } => {
26885            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26886        }
26887        Expr::Like { expr, pattern, .. } => {
26888            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26889        }
26890        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26891            args.iter().any(|x| expr_sublink_mentions(x, name))
26892        }
26893        Expr::InList { expr, list, .. } => {
26894            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26895        }
26896        Expr::ArraySubscript { target, index } => {
26897            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26898        }
26899        Expr::ArraySlice { target, lo, hi } => {
26900            expr_sublink_mentions(target, name)
26901                || lo
26902                    .as_deref()
26903                    .is_some_and(|x| expr_sublink_mentions(x, name))
26904                || hi
26905                    .as_deref()
26906                    .is_some_and(|x| expr_sublink_mentions(x, name))
26907        }
26908        Expr::AnyAll { expr, array, .. } => {
26909            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26910        }
26911        Expr::Case {
26912            operand,
26913            branches,
26914            else_branch,
26915        } => {
26916            operand
26917                .as_deref()
26918                .is_some_and(|x| expr_sublink_mentions(x, name))
26919                || branches
26920                    .iter()
26921                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26922                || else_branch
26923                    .as_deref()
26924                    .is_some_and(|x| expr_sublink_mentions(x, name))
26925        }
26926        _ => false,
26927    }
26928}
26929
26930/// Does this SELECT (in full — FROM tables, derived tables, its own
26931/// sublinks, and union arms) mention the named table?
26932fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26933    if let Some(from) = &s.from {
26934        if from.primary.name.eq_ignore_ascii_case(name) {
26935            return true;
26936        }
26937        if let Some(sub) = &from.primary.lateral_subquery
26938            && select_mentions_table(sub, name)
26939        {
26940            return true;
26941        }
26942        for j in &from.joins {
26943            if j.table.name.eq_ignore_ascii_case(name) {
26944                return true;
26945            }
26946            if let Some(sub) = &j.table.lateral_subquery
26947                && select_mentions_table(sub, name)
26948            {
26949                return true;
26950            }
26951        }
26952    }
26953    if select_has_self_ref_in_sublink(s, name) {
26954        return true;
26955    }
26956    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26957}
26958
26959/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26960/// row count, the way PG evaluates one before applying it.
26961///
26962/// `None` = not a constant (a column, a subquery, a function call).
26963/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26964/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26965/// All wordings were read off live PG 18.4.
26966fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26967    use crate::ast::{BinOp, Expr, Literal, UnOp};
26968    match e {
26969        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26970        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26971            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26972        }
26973        // PG coerces a string by its CONTENT, and fails on the value.
26974        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26975            |_| {
26976                Err(alloc::format!(
26977                    "invalid input syntax for type bigint: \"{t}\""
26978                ))
26979            },
26980            |n| Ok(i128::from(n)),
26981        )),
26982        Expr::Literal(Literal::Bool(_)) => Some(Err(
26983            "argument of {L} must be type bigint, not type boolean".into(),
26984        )),
26985        Expr::Unary {
26986            op: UnOp::Neg,
26987            expr,
26988        } => match fold_limit_constant(expr)? {
26989            Ok(v) => Some(Ok(-v)),
26990            e @ Err(_) => Some(e),
26991        },
26992        Expr::Binary { lhs, op, rhs } => {
26993            let a = match fold_limit_constant(lhs)? {
26994                Ok(v) => v,
26995                e @ Err(_) => return Some(e),
26996            };
26997            let b = match fold_limit_constant(rhs)? {
26998                Ok(v) => v,
26999                e @ Err(_) => return Some(e),
27000            };
27001            let out = match op {
27002                BinOp::Add => a.checked_add(b),
27003                BinOp::Sub => a.checked_sub(b),
27004                BinOp::Mul => a.checked_mul(b),
27005                BinOp::Div if b != 0 => a.checked_div(b),
27006                BinOp::Div => return Some(Err("division by zero".into())),
27007                BinOp::Mod if b != 0 => a.checked_rem(b),
27008                BinOp::Mod => return Some(Err("division by zero".into())),
27009                _ => return None,
27010            };
27011            // PG evaluates the arithmetic in the operand's own type, so an
27012            // int-by-int product that leaves int range fails there — before
27013            // the row count is ever looked at.
27014            match out {
27015                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27016                    Some(Err("integer out of range".into()))
27017                }
27018                Some(v) => Some(Ok(v)),
27019                None => Some(Err("integer out of range".into())),
27020            }
27021        }
27022        _ => None,
27023    }
27024}
27025
27026/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27027/// cast, which is what makes `LIMIT 2.5` keep three rows.
27028fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27029    if scale == 0 {
27030        return unscaled;
27031    }
27032    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27033        return 0;
27034    };
27035    let neg = unscaled < 0;
27036    let mag = unscaled.unsigned_abs() as i128;
27037    let rounded = (mag + div / 2) / div;
27038    if neg { -rounded } else { rounded }
27039}
27040
27041#[cfg(test)]
27042mod tests {
27043    use super::*;
27044    use alloc::string::ToString;
27045
27046    fn parse(s: &str) -> Statement {
27047        parse_statement(s).expect("parse ok")
27048    }
27049
27050    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27051    // `tables`, `partition`, etc. are unreserved keywords per PG's
27052    // `pg_get_keywords()` and MUST be usable as column / table /
27053    // alias names. Pre-T4 every drop-in user whose schema had one
27054    // of these as a column name (sentori events.release, mailrs
27055    // messages.index in some forks) blew the parser up at CREATE
27056    // TABLE time with "expected identifier, got Release". The
27057    // generalisation lives in `unreserved_keyword_text` + the
27058    // `expect_ident_like` and `parse_atom` arms that consult it.
27059    #[test]
27060    fn release_usable_as_column_name_in_create_table() {
27061        let stmt =
27062            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27063        if let Statement::CreateTable(t) = stmt {
27064            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27065            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27066        } else {
27067            panic!("expected CreateTable");
27068        }
27069    }
27070
27071    #[test]
27072    fn release_usable_as_column_ref_in_select_projection() {
27073        // The sentori `0003_partition_events.sql` INSERT-SELECT
27074        // walk references `release` in both column lists; the
27075        // projection-side use exercises `parse_atom`'s relaxed
27076        // identifier set.
27077        parse("SELECT id, release, payload FROM events WHERE id = 1");
27078    }
27079
27080    #[test]
27081    fn release_usable_as_column_ref_in_insert_column_list() {
27082        // INSERT INTO t (id, release, payload) VALUES (…)
27083        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27084    }
27085
27086    #[test]
27087    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27088        // Sentori `0013_audit_tombstone.sql` issues
27089        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27090        // emits Token::Drop (not Ident("drop")); the parser must
27091        // accept both in the ALTER COLUMN sub-dispatch.
27092        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27093    }
27094
27095    #[test]
27096    fn create_index_accepts_parenthesised_expression_key() {
27097        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27098        // expression index. Pre-T4 the parser bailed at the
27099        // inner `(` with "expected column ident or expression,
27100        // got LParen". The Token::LParen arm in CREATE INDEX
27101        // routes through the expression parser instead.
27102        parse(
27103            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27104             ON events ((payload->'bundle'->>'id'))",
27105        );
27106    }
27107
27108    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27109    // surface as parse errors, never stack overflows (embed hosts
27110    // abort on overflow).
27111    /// The nesting budget is a COUNT; what it has to fit inside is a
27112    /// number of BYTES, and only one of those two is stable across
27113    /// compiler versions. Round 847 measured 30,336 bytes per level
27114    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27115    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27116    /// aborted instead of erroring, which is precisely the outcome it
27117    /// exists to rule out.
27118    ///
27119    /// So the budget is metered rather than assumed. The ceiling leaves
27120    /// the depth SPG advertises fitting in a default 2 MiB thread with
27121    /// room to spare, in the debug build, where frames are widest.
27122    #[test]
27123    fn nesting_frame_cost_stays_under_ceiling() {
27124        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27125        // thread keeps a margin for whatever called the parser.
27126        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27127
27128        frame_meter::reset();
27129        let depth = frame_meter::SAMPLE_HI + 8;
27130        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27131        parse(&sql);
27132
27133        let per_level = frame_meter::bytes_per_level();
27134        {
27135            extern crate std;
27136            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27137        }
27138        assert!(
27139            per_level <= CEILING,
27140            "{per_level} bytes per nesting level exceeds {CEILING}; \
27141             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27142             in parse_expr_inner / parse_unary rather than lowering the \
27143             depth or widening the stack.",
27144            per_level * MAX_NEST_DEPTH
27145        );
27146    }
27147
27148    #[test]
27149    fn nesting_budget_errors_cleanly() {
27150        let depth = MAX_NEST_DEPTH + 50;
27151        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27152        let err = parse_statement(&sql).expect_err("must reject");
27153        assert!(err.message.contains("nests deeper"), "{err:?}");
27154        // Within budget still parses.
27155        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27156        parse(&sql);
27157    }
27158
27159    #[test]
27160    fn binary_chain_budget_errors_cleanly() {
27161        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27162        let err = parse_statement(&sql).expect_err("must reject");
27163        assert!(err.message.contains("chained binary"), "{err:?}");
27164        // Within budget still parses (chain depth ≤ budget is safe
27165        // for recursive eval/drop on 2 MiB stacks).
27166        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27167        parse(&sql);
27168    }
27169
27170    #[test]
27171    fn in_list_unaffected_by_chain_budget() {
27172        // Flat InList: 20k elements parse fine and stay flat.
27173        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27174        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27175        let Statement::Select(s) = parse(&sql) else {
27176            panic!("expected select")
27177        };
27178        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27179            panic!("expected flat InList, got {:?}", s.where_)
27180        };
27181        assert_eq!(list.len(), 20_000);
27182        assert!(!negated);
27183    }
27184
27185    fn lit_int(n: i64) -> Expr {
27186        Expr::Literal(Literal::Integer(n))
27187    }
27188
27189    fn col(name: &str) -> Expr {
27190        Expr::Column(ColumnName {
27191            qualifier: None,
27192            name: name.into(),
27193        })
27194    }
27195
27196    #[test]
27197    fn select_single_integer() {
27198        let s = parse("SELECT 1");
27199        let Statement::Select(s) = s else {
27200            panic!("expected SELECT")
27201        };
27202        assert_eq!(s.items.len(), 1);
27203        assert!(s.from.is_none());
27204        assert!(s.where_.is_none());
27205    }
27206
27207    #[test]
27208    fn select_multiple_literal_kinds() {
27209        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27210        let Statement::Select(s) = s else {
27211            panic!("expected SELECT")
27212        };
27213        assert_eq!(s.items.len(), 5);
27214    }
27215
27216    #[test]
27217    fn select_wildcard_from_table() {
27218        let s = parse("SELECT * FROM users");
27219        let Statement::Select(s) = s else {
27220            panic!("expected SELECT")
27221        };
27222        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27223        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27224    }
27225
27226    #[test]
27227    fn select_with_table_alias() {
27228        let s = parse("SELECT * FROM users AS u");
27229        let Statement::Select(s) = s else {
27230            panic!("expected SELECT")
27231        };
27232        let t = &s.from.as_ref().unwrap().primary;
27233        assert_eq!(t.name, "users");
27234        assert_eq!(t.alias.as_deref(), Some("u"));
27235    }
27236
27237    #[test]
27238    fn select_with_where_eq() {
27239        let s = parse("SELECT a FROM t WHERE a = 1");
27240        let Statement::Select(s) = s else {
27241            panic!("expected SELECT")
27242        };
27243        let w = s.where_.unwrap();
27244        assert_eq!(
27245            w,
27246            Expr::Binary {
27247                lhs: Box::new(col("a")),
27248                op: BinOp::Eq,
27249                rhs: Box::new(lit_int(1)),
27250            }
27251        );
27252    }
27253
27254    #[test]
27255    fn arithmetic_precedence() {
27256        let s = parse("SELECT 1 + 2 * 3");
27257        let Statement::Select(s) = s else {
27258            panic!("expected SELECT")
27259        };
27260        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27261            panic!("wildcard?")
27262        };
27263        assert_eq!(
27264            expr,
27265            &Expr::Binary {
27266                lhs: Box::new(lit_int(1)),
27267                op: BinOp::Add,
27268                rhs: Box::new(Expr::Binary {
27269                    lhs: Box::new(lit_int(2)),
27270                    op: BinOp::Mul,
27271                    rhs: Box::new(lit_int(3)),
27272                }),
27273            }
27274        );
27275    }
27276
27277    #[test]
27278    fn parentheses_override_precedence() {
27279        let s = parse("SELECT (1 + 2) * 3");
27280        let Statement::Select(s) = s else {
27281            panic!("expected SELECT")
27282        };
27283        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27284            panic!()
27285        };
27286        assert_eq!(
27287            expr,
27288            &Expr::Binary {
27289                lhs: Box::new(Expr::Binary {
27290                    lhs: Box::new(lit_int(1)),
27291                    op: BinOp::Add,
27292                    rhs: Box::new(lit_int(2)),
27293                }),
27294                op: BinOp::Mul,
27295                rhs: Box::new(lit_int(3)),
27296            }
27297        );
27298    }
27299
27300    #[test]
27301    fn not_binds_below_comparison() {
27302        // `NOT a = 1` should parse as `NOT (a = 1)`.
27303        let s = parse("SELECT NOT a = 1 FROM t");
27304        let Statement::Select(s) = s else {
27305            panic!("expected SELECT")
27306        };
27307        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27308            panic!()
27309        };
27310        assert_eq!(
27311            expr,
27312            &Expr::Unary {
27313                op: UnOp::Not,
27314                expr: Box::new(Expr::Binary {
27315                    lhs: Box::new(col("a")),
27316                    op: BinOp::Eq,
27317                    rhs: Box::new(lit_int(1)),
27318                }),
27319            }
27320        );
27321    }
27322
27323    #[test]
27324    fn unary_minus_binds_above_multiplication() {
27325        // `-a * 2` should be `(-a) * 2`.
27326        let s = parse("SELECT -a * 2 FROM t");
27327        let Statement::Select(s) = s else {
27328            panic!("expected SELECT")
27329        };
27330        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27331            panic!()
27332        };
27333        assert_eq!(
27334            expr,
27335            &Expr::Binary {
27336                lhs: Box::new(Expr::Unary {
27337                    op: UnOp::Neg,
27338                    expr: Box::new(col("a")),
27339                }),
27340                op: BinOp::Mul,
27341                rhs: Box::new(lit_int(2)),
27342            }
27343        );
27344    }
27345
27346    #[test]
27347    fn qualified_column() {
27348        let s = parse("SELECT t.col FROM t");
27349        let Statement::Select(s) = s else {
27350            panic!("expected SELECT")
27351        };
27352        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27353            panic!()
27354        };
27355        assert_eq!(
27356            expr,
27357            &Expr::Column(ColumnName {
27358                qualifier: Some("t".into()),
27359                name: "col".into()
27360            })
27361        );
27362    }
27363
27364    #[test]
27365    fn select_item_alias_with_as() {
27366        let s = parse("SELECT a AS y FROM t");
27367        let Statement::Select(s) = s else {
27368            panic!("expected SELECT")
27369        };
27370        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27371            panic!()
27372        };
27373        assert_eq!(alias.as_deref(), Some("y"));
27374    }
27375
27376    #[test]
27377    fn trailing_semicolon_accepted() {
27378        let s = parse("SELECT 1;");
27379        let Statement::Select(s) = s else {
27380            panic!("expected SELECT")
27381        };
27382        assert_eq!(s.items.len(), 1);
27383    }
27384
27385    #[test]
27386    fn boolean_chain_with_and_or_not() {
27387        // (NOT a) OR (b AND (NOT c))
27388        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27389        let Statement::Select(s) = s else {
27390            panic!("expected SELECT")
27391        };
27392        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27393            panic!()
27394        };
27395        let expected = Expr::Binary {
27396            lhs: Box::new(Expr::Unary {
27397                op: UnOp::Not,
27398                expr: Box::new(col("a")),
27399            }),
27400            op: BinOp::Or,
27401            rhs: Box::new(Expr::Binary {
27402                lhs: Box::new(col("b")),
27403                op: BinOp::And,
27404                rhs: Box::new(Expr::Unary {
27405                    op: UnOp::Not,
27406                    expr: Box::new(col("c")),
27407                }),
27408            }),
27409        };
27410        assert_eq!(expr, &expected);
27411    }
27412
27413    #[test]
27414    fn empty_input_errors() {
27415        // v7.14.0 — pg_dump preambles emit several comment-only
27416        // / blank-line statements that collapse to Statement::
27417        // Empty rather than a parse error. The old "SELECT in
27418        // message" assertion is stale; verify the new contract:
27419        // empty / whitespace / comment-only input parses to
27420        // Statement::Empty.
27421        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27422        assert!(matches!(
27423            parse_statement("  \n\t ").unwrap(),
27424            Statement::Empty
27425        ));
27426        // Sanity: malformed-but-non-empty still errors.
27427        assert!(parse_statement("SELECT FROM WHERE").is_err());
27428    }
27429
27430    #[test]
27431    fn unmatched_paren_errors() {
27432        assert!(parse_statement("SELECT (1 + 2").is_err());
27433    }
27434
27435    #[test]
27436    fn display_round_trip_simple_select() {
27437        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27438        let text = original.to_string();
27439        let again = parse_statement(&text).expect("re-parse");
27440        assert_eq!(original, again);
27441    }
27442
27443    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27444
27445    #[test]
27446    fn create_table_single_column() {
27447        let s = parse("CREATE TABLE foo (a INT)");
27448        let Statement::CreateTable(c) = s else {
27449            panic!("expected CreateTable")
27450        };
27451        assert_eq!(c.name, "foo");
27452        assert_eq!(c.columns.len(), 1);
27453        assert_eq!(c.columns[0].name, "a");
27454        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27455        assert!(c.columns[0].nullable);
27456    }
27457
27458    #[test]
27459    fn create_table_multi_column_with_not_null_mix() {
27460        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27461        let Statement::CreateTable(c) = s else {
27462            panic!()
27463        };
27464        assert_eq!(c.columns.len(), 4);
27465        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27466        assert!(!c.columns[0].nullable);
27467        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27468        assert!(c.columns[1].nullable);
27469        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27470        assert!(!c.columns[2].nullable);
27471        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27472    }
27473
27474    #[test]
27475    fn create_table_bigint_supported() {
27476        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27477        let Statement::CreateTable(c) = s else {
27478            panic!()
27479        };
27480        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27481    }
27482
27483    #[test]
27484    fn create_table_vector_default_is_f32() {
27485        let s = parse("CREATE TABLE t (v VECTOR(128))");
27486        let Statement::CreateTable(c) = s else {
27487            panic!()
27488        };
27489        assert_eq!(
27490            c.columns[0].ty,
27491            ColumnTypeName::Vector {
27492                dim: 128,
27493                encoding: VecEncoding::F32,
27494            },
27495        );
27496    }
27497
27498    #[test]
27499    fn create_table_vector_using_sq8() {
27500        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27501        // Case-insensitive on both `USING` and the encoding name.
27502        for sql in [
27503            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27504            "CREATE TABLE t (v VECTOR(128) using sq8)",
27505        ] {
27506            let s = parse(sql);
27507            let Statement::CreateTable(c) = s else {
27508                panic!()
27509            };
27510            assert_eq!(
27511                c.columns[0].ty,
27512                ColumnTypeName::Vector {
27513                    dim: 128,
27514                    encoding: VecEncoding::Sq8,
27515                },
27516                "{sql}",
27517            );
27518        }
27519    }
27520
27521    #[test]
27522    fn create_table_vector_using_unknown_errors() {
27523        // v7.16.1 — the inline `USING <encoding>` shape on
27524        // CREATE TABLE column defs was withdrawn before
27525        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27526        // (col vector_<metric>_ops)`; the parser now rejects
27527        // USING at column-list position with a clearer
27528        // "expected ',' or ')'" message. Test asserts the
27529        // current rejection, not the old "unknown vector
27530        // encoding" string.
27531        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27532        assert!(
27533            err.message.contains("USING")
27534                || err.message.contains("using")
27535                || err.message.contains("')'")
27536                || err.message.contains("','"),
27537            "expected USING/column-list rejection, got: {}",
27538            err.message
27539        );
27540    }
27541
27542    #[test]
27543    fn vector_using_sq8_display_roundtrips() {
27544        // The Display impl must produce text that re-parses to the
27545        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27546        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27547        let Statement::CreateTable(c) = s else {
27548            panic!()
27549        };
27550        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27551    }
27552
27553    #[test]
27554    fn parser_recognises_placeholders() {
27555        use crate::ast::{Expr, SelectItem, Statement};
27556        // $N in expression position parses as Expr::Placeholder(N).
27557        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27558        let Statement::Select(sel) = s else { panic!() };
27559        assert!(matches!(
27560            sel.items[0],
27561            SelectItem::Expr {
27562                expr: Expr::Placeholder(1),
27563                alias: None
27564            }
27565        ));
27566        // $2 + 1
27567        let SelectItem::Expr {
27568            expr: Expr::Binary { lhs, rhs, .. },
27569            ..
27570        } = &sel.items[1]
27571        else {
27572            panic!()
27573        };
27574        assert!(matches!(**lhs, Expr::Placeholder(2)));
27575        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27576        // WHERE x = $3
27577        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27578            panic!()
27579        };
27580        assert!(matches!(**rhs, Expr::Placeholder(3)));
27581    }
27582
27583    #[test]
27584    fn parser_rejects_dollar_zero() {
27585        // $0 is not valid in PG; the lexer rejects it.
27586        assert!(parse_statement("SELECT $0").is_err());
27587    }
27588
27589    #[test]
27590    fn placeholder_display_roundtrips() {
27591        // The Display impl must produce text that re-lexes to the
27592        // same Placeholder token.
27593        let s = parse("SELECT $42 FROM t");
27594        let printed = s.to_string();
27595        assert!(printed.contains("$42"));
27596        let again = parse(&printed);
27597        assert_eq!(s, again);
27598    }
27599
27600    #[test]
27601    fn alter_index_rebuild_bare() {
27602        use crate::ast::{AlterIndexTarget, Statement};
27603        let s = parse("ALTER INDEX my_idx REBUILD");
27604        let Statement::AlterIndex(a) = s else {
27605            panic!("expected AlterIndex, got {s:?}")
27606        };
27607        assert_eq!(a.name, "my_idx");
27608        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27609    }
27610
27611    #[test]
27612    fn alter_index_rebuild_with_encoding() {
27613        use crate::ast::{AlterIndexTarget, Statement};
27614        for (sql, want) in [
27615            (
27616                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27617                VecEncoding::F32,
27618            ),
27619            (
27620                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27621                VecEncoding::Sq8,
27622            ),
27623            (
27624                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27625                VecEncoding::F16,
27626            ),
27627        ] {
27628            let s = parse(sql);
27629            let Statement::AlterIndex(a) = s else {
27630                panic!("{sql}: expected AlterIndex")
27631            };
27632            assert_eq!(a.name, "my_idx");
27633            assert_eq!(
27634                a.target,
27635                AlterIndexTarget::Rebuild {
27636                    encoding: Some(want)
27637                },
27638                "{sql}"
27639            );
27640        }
27641    }
27642
27643    #[test]
27644    fn alter_index_rebuild_unknown_encoding_errors() {
27645        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27646        assert!(
27647            err.message.contains("unknown vector encoding"),
27648            "got: {}",
27649            err.message
27650        );
27651    }
27652
27653    #[test]
27654    fn alter_index_rebuild_display_roundtrips() {
27655        for (input, want) in [
27656            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27657            (
27658                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27659                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27660            ),
27661            (
27662                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27663                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27664            ),
27665        ] {
27666            let s = parse(input);
27667            assert_eq!(s.to_string(), want);
27668        }
27669    }
27670
27671    #[test]
27672    fn create_table_unknown_type_defers_to_engine() {
27673        // v4.9 picked XML as a parse-time "unsupported column
27674        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27675        // an unknown type ident parses as Text + `user_type_ref`
27676        // so CREATE TABLE can resolve user-defined enum / domain
27677        // types — rejection of truly-unknown types moved to the
27678        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27679        // to a first-class built-in, so this probe switched to a
27680        // synthetic name nothing in the lexer will ever recognise.
27681        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27682        let Statement::CreateTable(t) = stmt else {
27683            panic!("expected CreateTable");
27684        };
27685        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27686    }
27687
27688    #[test]
27689    fn create_table_missing_table_keyword_errors() {
27690        assert!(parse_statement("CREATE x (a INT)").is_err());
27691    }
27692
27693    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27694    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27695
27696    #[test]
27697    fn parse_create_table_partition_by_range() {
27698        use crate::ast::{PartitionBySpec, PartitionKindAst};
27699        let stmt = parse_statement(
27700            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27701             payload JSONB) PARTITION BY RANGE (ts)",
27702        )
27703        .unwrap();
27704        let Statement::CreateTable(t) = stmt else {
27705            panic!("expected CreateTable");
27706        };
27707        assert!(t.partition_of.is_none(), "parent has no partition_of");
27708        assert_eq!(t.columns.len(), 3);
27709        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27710        assert_eq!(
27711            by,
27712            &PartitionBySpec {
27713                kind: PartitionKindAst::Range,
27714                key_columns: alloc::vec!["ts".to_string()],
27715            }
27716        );
27717        // Display round-trip preserves the suffix. `quote_ident`
27718        // only adds double quotes when the ident needs escaping, so
27719        // a plain `ts` survives bare here.
27720        assert!(
27721            t.to_string().contains("PARTITION BY RANGE (ts)"),
27722            "Display lost PARTITION BY suffix: {t}"
27723        );
27724    }
27725
27726    #[test]
27727    fn parse_create_table_partition_of_range() {
27728        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27729        let stmt = parse_statement(
27730            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27731             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27732        )
27733        .unwrap();
27734        let Statement::CreateTable(t) = stmt else {
27735            panic!("expected CreateTable");
27736        };
27737        assert!(t.columns.is_empty(), "child inherits columns from parent");
27738        assert!(t.partition_by.is_none());
27739        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27740        assert_eq!(of.parent_name, "events_partitioned");
27741        let PartitionOfSpec { bounds, .. } = of.clone();
27742        match bounds {
27743            PartitionOfBoundsAst::Range { lower, upper } => {
27744                assert!(lower.to_string().contains("2026-06-01"));
27745                assert!(upper.to_string().contains("2026-07-01"));
27746            }
27747            other => panic!("expected Range, got {other:?}"),
27748        }
27749        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27750        // skips quotes when not required, so the parent name appears
27751        // bare here.
27752        let s = t.to_string();
27753        assert!(
27754            s.contains("PARTITION OF events_partitioned"),
27755            "Display lost PARTITION OF: {s}"
27756        );
27757        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27758        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27759    }
27760
27761    #[test]
27762    fn parse_create_table_partition_of_default() {
27763        use crate::ast::PartitionOfBoundsAst;
27764        let stmt =
27765            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27766                .unwrap();
27767        let Statement::CreateTable(t) = stmt else {
27768            panic!("expected CreateTable");
27769        };
27770        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27771        assert_eq!(of.parent_name, "events_partitioned");
27772        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27773        assert!(
27774            t.to_string()
27775                .contains("PARTITION OF events_partitioned DEFAULT"),
27776            "Display lost DEFAULT: {t}"
27777        );
27778    }
27779
27780    #[test]
27781    fn parse_create_table_partition_by_list() {
27782        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27783        // child with `FOR VALUES IN (lit, lit, …)`.
27784        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27785        let parent =
27786            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27787                .unwrap();
27788        let Statement::CreateTable(t) = parent else {
27789            panic!("expected CreateTable");
27790        };
27791        let Some(PartitionBySpec {
27792            kind,
27793            ref key_columns,
27794        }) = t.partition_by
27795        else {
27796            panic!("expected PARTITION BY");
27797        };
27798        assert_eq!(kind, PartitionKindAst::List);
27799        assert_eq!(*key_columns, vec!["region".to_string()]);
27800        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27801
27802        let child = parse_statement(
27803            "CREATE TABLE events_apac PARTITION OF events_listed \
27804             FOR VALUES IN ('jp', 'kr', 'tw')",
27805        )
27806        .unwrap();
27807        let Statement::CreateTable(c) = child else {
27808            panic!("expected CreateTable");
27809        };
27810        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27811        let PartitionOfBoundsAst::List { values } = &of.bounds else {
27812            panic!("expected List bounds, got {:?}", of.bounds);
27813        };
27814        assert_eq!(values.len(), 3);
27815        let disp = c.to_string();
27816        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27817    }
27818
27819    #[test]
27820    fn parse_create_table_partition_by_hash() {
27821        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27822        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27823        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27824        let parent =
27825            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27826        let Statement::CreateTable(t) = parent else {
27827            panic!("expected CreateTable");
27828        };
27829        let Some(PartitionBySpec {
27830            kind,
27831            ref key_columns,
27832        }) = t.partition_by
27833        else {
27834            panic!("expected PARTITION BY");
27835        };
27836        assert_eq!(kind, PartitionKindAst::Hash);
27837        assert_eq!(*key_columns, vec!["id".to_string()]);
27838        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27839
27840        let child = parse_statement(
27841            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27842             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27843        )
27844        .unwrap();
27845        let Statement::CreateTable(c) = child else {
27846            panic!("expected CreateTable");
27847        };
27848        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27849        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27850            panic!("expected Hash bounds");
27851        };
27852        assert_eq!(modulus, 4);
27853        assert_eq!(remainder, 0);
27854        let disp = c.to_string();
27855        assert!(
27856            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27857            "Display lost HASH bounds: {disp}"
27858        );
27859
27860        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27861        let bad = parse_statement(
27862            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27863             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27864        );
27865        let msg = format!("{}", bad.unwrap_err());
27866        assert!(
27867            msg.contains("REMAINDER") && msg.contains("MODULUS"),
27868            "expected REMAINDER/MODULUS validation error: {msg}"
27869        );
27870    }
27871
27872    #[test]
27873    fn parse_create_table_partition_of_rejects_columns() {
27874        // v7.37.6-B contract: PARTITION OF children inherit columns
27875        // from the parent; an explicit list MUST surface as a parse
27876        // error rather than getting silently ignored.
27877        let err = parse_statement(
27878            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27879             FOR VALUES FROM ('a') TO ('b')",
27880        );
27881        assert!(err.is_err(), "expected parse error for explicit columns");
27882        let msg = format!("{}", err.unwrap_err());
27883        assert!(
27884            msg.contains("PARTITION OF") && msg.contains("column"),
27885            "error should mention PARTITION OF + columns: {msg}"
27886        );
27887    }
27888
27889    #[test]
27890    fn insert_single_value() {
27891        let s = parse("INSERT INTO foo VALUES (42)");
27892        let Statement::Insert(i) = s else {
27893            panic!("expected Insert")
27894        };
27895        assert_eq!(i.table, "foo");
27896        assert_eq!(i.rows.len(), 1);
27897        assert_eq!(i.rows[0].len(), 1);
27898        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27899    }
27900
27901    #[test]
27902    fn insert_multi_value_with_mixed_literals() {
27903        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27904        let Statement::Insert(i) = s else { panic!() };
27905        assert_eq!(i.rows.len(), 1);
27906        assert_eq!(i.rows[0].len(), 5);
27907    }
27908
27909    #[test]
27910    fn insert_missing_into_errors() {
27911        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27912    }
27913
27914    #[test]
27915    fn create_table_round_trip() {
27916        let original =
27917            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27918        let text = original.to_string();
27919        let again = parse_statement(&text).expect("re-parse");
27920        assert_eq!(original, again);
27921    }
27922
27923    #[test]
27924    fn insert_round_trip_with_negation_and_string() {
27925        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27926        let text = original.to_string();
27927        let again = parse_statement(&text).expect("re-parse");
27928        assert_eq!(original, again);
27929    }
27930
27931    #[test]
27932    fn unknown_keyword_at_statement_start_errors() {
27933        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27934        // the top-level dispatch still has no branch to take.
27935        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27936        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27937    }
27938
27939    // --- v0.8 CREATE INDEX --------------------------------------------------
27940
27941    #[test]
27942    fn create_index_basic() {
27943        let s = parse("CREATE INDEX idx_id ON users (id)");
27944        let Statement::CreateIndex(c) = s else {
27945            panic!("expected CreateIndex")
27946        };
27947        assert_eq!(c.name, "idx_id");
27948        assert_eq!(c.table, "users");
27949        assert_eq!(c.column, "id");
27950    }
27951
27952    #[test]
27953    fn create_index_missing_on_errors() {
27954        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27955    }
27956
27957    #[test]
27958    fn create_index_missing_paren_errors() {
27959        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27960    }
27961
27962    #[test]
27963    fn create_index_round_trip() {
27964        let original = parse("CREATE INDEX by_name ON users (name)");
27965        let again = parse_statement(&original.to_string()).unwrap();
27966        assert_eq!(original, again);
27967    }
27968
27969    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27970
27971    #[test]
27972    fn create_unique_index_basic() {
27973        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27974        let Statement::CreateIndex(c) = s else {
27975            panic!("expected CreateIndex");
27976        };
27977        assert!(c.is_unique);
27978        assert_eq!(c.column, "a");
27979        assert!(c.partial_predicate.is_none());
27980    }
27981
27982    #[test]
27983    fn create_unique_index_partial() {
27984        // mailrs's email_templates "one default per user" shape.
27985        let s = parse(
27986            "CREATE UNIQUE INDEX idx_email_templates_user_default \
27987             ON email_templates (user_address) WHERE is_default = true",
27988        );
27989        let Statement::CreateIndex(c) = s else {
27990            panic!("expected CreateIndex");
27991        };
27992        assert!(c.is_unique);
27993        assert_eq!(c.table, "email_templates");
27994        assert_eq!(c.column, "user_address");
27995        assert!(c.partial_predicate.is_some());
27996    }
27997
27998    #[test]
27999    fn create_unique_index_composite_with_predicate() {
28000        // mailrs's calendar_events instance: composite columns.
28001        let s = parse(
28002            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28003             ON calendar_events (calendar_id, uid, recurrence_id) \
28004             WHERE recurrence_id IS NOT NULL",
28005        );
28006        let Statement::CreateIndex(c) = s else {
28007            panic!("expected CreateIndex");
28008        };
28009        assert!(c.is_unique);
28010        assert_eq!(c.column, "calendar_id");
28011        assert_eq!(
28012            c.extra_columns,
28013            vec!["uid".to_string(), "recurrence_id".to_string()]
28014        );
28015        assert!(c.partial_predicate.is_some());
28016    }
28017
28018    #[test]
28019    fn create_unique_index_using_btree_ok() {
28020        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28021        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28022    }
28023
28024    #[test]
28025    fn create_unique_index_using_hnsw_rejected() {
28026        let err =
28027            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28028        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28029    }
28030
28031    #[test]
28032    fn create_unique_index_round_trip() {
28033        let original = parse(
28034            "CREATE UNIQUE INDEX uq_calendar_events_master \
28035             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28036        );
28037        let again = parse_statement(&original.to_string()).unwrap();
28038        assert_eq!(original, again);
28039    }
28040
28041    #[test]
28042    fn create_unique_without_index_errors() {
28043        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28044        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28045        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28046    }
28047
28048    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28049
28050    #[test]
28051    fn create_table_bytea_column() {
28052        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28053        let Statement::CreateTable(c) = s else {
28054            panic!("expected CreateTable");
28055        };
28056        assert_eq!(c.columns.len(), 2);
28057        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28058        assert!(!c.columns[1].nullable);
28059    }
28060
28061    #[test]
28062    fn create_table_bytes_alias_column() {
28063        let s = parse("CREATE TABLE t (blob BYTES)");
28064        let Statement::CreateTable(c) = s else {
28065            panic!("expected CreateTable");
28066        };
28067        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28068    }
28069
28070    #[test]
28071    fn bytea_round_trip_display() {
28072        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28073        let again = parse_statement(&original.to_string()).unwrap();
28074        assert_eq!(original, again);
28075    }
28076
28077    // --- v0.9 transactions -------------------------------------------------
28078
28079    #[test]
28080    fn begin_commit_rollback_parse_as_unit_variants() {
28081        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28082        assert_eq!(parse("COMMIT"), Statement::Commit);
28083        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28084        // Trailing semicolons accepted too.
28085        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28086        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28087        // statement (with or without the WORK/TRANSACTION noise word).
28088        assert_eq!(
28089            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28090            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28091        );
28092        assert_eq!(
28093            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28094            Statement::Begin(Some(IsolationLevel::Serializable))
28095        );
28096        // A non-isolation mode keeps the session default (None).
28097        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28098    }
28099
28100    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28101
28102    #[test]
28103    fn inner_product_binop_parses() {
28104        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28105        let Statement::Select(s) = s else { panic!() };
28106        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28107            panic!()
28108        };
28109        assert!(matches!(
28110            expr,
28111            Expr::Binary {
28112                op: BinOp::InnerProduct,
28113                ..
28114            }
28115        ));
28116    }
28117
28118    #[test]
28119    fn cosine_distance_binop_parses() {
28120        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28121        let Statement::Select(s) = s else { panic!() };
28122        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28123            panic!()
28124        };
28125        assert!(matches!(
28126            expr,
28127            Expr::Binary {
28128                op: BinOp::CosineDistance,
28129                ..
28130            }
28131        ));
28132    }
28133
28134    #[test]
28135    fn vector_cast_postfix_wraps_string_literal() {
28136        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28137        let Statement::Select(s) = s else { panic!() };
28138        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28139            panic!()
28140        };
28141        assert!(matches!(
28142            expr,
28143            Expr::Cast {
28144                target: CastTarget::Vector,
28145                ..
28146            }
28147        ));
28148    }
28149
28150    #[test]
28151    fn unsupported_cast_target_errors() {
28152        // v7.37.5 ship triage promoted the parser to accept every
28153        // ident as a `CastTarget::Named(canonical)`; the engine
28154        // surfaces the "unsupported cast target" error at eval
28155        // time when `type_name_to_data_type` can't resolve it.
28156        // Parser-side error now requires a NON-ident after `::`
28157        // (e.g. a punctuation token).
28158        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28159        assert_eq!(err.message, "syntax error at or near \",\"");
28160    }
28161
28162    #[test]
28163    fn tx_statements_round_trip() {
28164        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28165            let original = parse(q);
28166            let again = parse_statement(&original.to_string()).unwrap();
28167            assert_eq!(original, again);
28168        }
28169    }
28170
28171    #[test]
28172    fn interval_text_parsing_units() {
28173        // v7.37.5 β — three-field shape `(months, days, micros)` so
28174        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28175        // Single unit.
28176        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28177        assert_eq!(
28178            parse_interval_text("24 hours"),
28179            Some((0, 0, 86_400_000_000))
28180        );
28181        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28182        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28183        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28184        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28185        // Compound spans accumulate per-dimension.
28186        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28187        assert_eq!(
28188            parse_interval_text("1 day 2 hours"),
28189            Some((0, 1, 7_200_000_000))
28190        );
28191        // Negative numbers carry through per-dimension.
28192        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28193        // Bad shapes return None.
28194        assert_eq!(parse_interval_text(""), None);
28195        assert_eq!(parse_interval_text("garbage"), None);
28196        assert_eq!(parse_interval_text("1 fortnight"), None);
28197        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28198        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28199        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28200        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28201        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28202    }
28203
28204    #[test]
28205    fn interval_literal_roundtrips_via_display() {
28206        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28207        let s = parsed.to_string();
28208        // Display preserves the original text verbatim.
28209        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28210        // And re-parsing yields a structurally equal statement.
28211        let again = parse_statement(&s).unwrap();
28212        assert_eq!(parsed, again);
28213    }
28214
28215    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28216
28217    #[test]
28218    fn parser_recognises_create_publication_bare() {
28219        let s = parse("CREATE PUBLICATION pub_a");
28220        let Statement::CreatePublication(p) = s else {
28221            panic!("expected CreatePublication, got {s:?}")
28222        };
28223        assert_eq!(p.name, "pub_a");
28224        assert_eq!(p.scope, PublicationScope::AllTables);
28225    }
28226
28227    #[test]
28228    fn parser_recognises_create_publication_for_all_tables() {
28229        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28230        let Statement::CreatePublication(p) = s else {
28231            panic!("expected CreatePublication, got {s:?}")
28232        };
28233        assert_eq!(p.name, "pub_a");
28234        assert_eq!(p.scope, PublicationScope::AllTables);
28235    }
28236
28237    #[test]
28238    fn parser_recognises_drop_publication() {
28239        let s = parse("DROP PUBLICATION pub_a");
28240        let Statement::DropPublication { name, .. } = s else {
28241            panic!("expected DropPublication, got {s:?}")
28242        };
28243        assert_eq!(name, "pub_a");
28244    }
28245
28246    #[test]
28247    fn parser_recognises_for_table_list() {
28248        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28249        let Statement::CreatePublication(p) = s else {
28250            panic!("expected CreatePublication, got {s:?}")
28251        };
28252        assert_eq!(p.name, "pub_a");
28253        let PublicationScope::ForTables(ts) = p.scope else {
28254            panic!("expected ForTables scope")
28255        };
28256        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28257    }
28258
28259    #[test]
28260    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28261        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28262        // is rejected (`invalid publication object list`; the old
28263        // test pinned an unverifiable "PG 19 accepts both" claim);
28264        // TABLES pairs with IN SCHEMA.
28265        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28266            .expect_err("bare FOR TABLES must reject");
28267        assert!(
28268            alloc::format!("{err}").contains("invalid publication object list"),
28269            "got: {err}"
28270        );
28271        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28272        let Statement::CreatePublication(p) = s else {
28273            panic!("expected CreatePublication, got {s:?}")
28274        };
28275        let PublicationScope::TablesInSchema(schema) = p.scope else {
28276            panic!("expected TablesInSchema")
28277        };
28278        assert_eq!(schema, "public");
28279    }
28280
28281    #[test]
28282    fn parser_recognises_for_all_tables_except_list() {
28283        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28284        let Statement::CreatePublication(p) = s else {
28285            panic!()
28286        };
28287        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28288            panic!("expected AllTablesExcept")
28289        };
28290        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28291    }
28292
28293    #[test]
28294    fn parser_rejects_for_table_with_empty_list() {
28295        // `FOR TABLE` with nothing after is a parse error.
28296        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28297            .expect_err("must error on empty list");
28298        // No specific message asserted — the call falls through to
28299        // expect_ident_like which yields "expected identifier, got …".
28300        assert!(!err.message.is_empty());
28301    }
28302
28303    #[test]
28304    fn parser_recognises_show_publications() {
28305        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28306        // bare ident in this position, NOT a reserved keyword.
28307        let s = parse("SHOW PUBLICATIONS");
28308        assert!(matches!(s, Statement::ShowPublications));
28309    }
28310
28311    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28312
28313    #[test]
28314    fn parser_recognises_create_subscription_single_publication() {
28315        let s = parse(
28316            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28317        );
28318        let Statement::CreateSubscription(c) = s else {
28319            panic!("expected CreateSubscription, got {s:?}")
28320        };
28321        assert_eq!(c.name, "sub_a");
28322        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28323        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28324    }
28325
28326    #[test]
28327    fn parser_recognises_create_subscription_multi_publication() {
28328        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28329        let Statement::CreateSubscription(c) = s else {
28330            panic!()
28331        };
28332        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28333    }
28334
28335    #[test]
28336    fn parser_rejects_create_subscription_missing_connection() {
28337        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28338            .expect_err("must error on missing CONNECTION");
28339        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28340    }
28341
28342    #[test]
28343    fn parser_rejects_create_subscription_missing_publication() {
28344        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28345            .expect_err("must error on missing PUBLICATION");
28346        assert_eq!(err.message, "syntax error at end of input");
28347    }
28348
28349    #[test]
28350    fn parser_recognises_drop_subscription() {
28351        let s = parse("DROP SUBSCRIPTION sub_a");
28352        let Statement::DropSubscription { name, .. } = s else {
28353            panic!("expected DropSubscription, got {s:?}")
28354        };
28355        assert_eq!(name, "sub_a");
28356    }
28357
28358    #[test]
28359    fn parser_recognises_show_subscriptions() {
28360        let s = parse("SHOW SUBSCRIPTIONS");
28361        assert!(matches!(s, Statement::ShowSubscriptions));
28362    }
28363
28364    #[test]
28365    fn parser_recognises_wait_for_wal_position_no_timeout() {
28366        let s = parse("WAIT FOR WAL POSITION 12345");
28367        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28368            panic!("expected WaitForWalPosition, got {s:?}")
28369        };
28370        assert_eq!(pos, 12345);
28371        assert!(timeout_ms.is_none());
28372    }
28373
28374    #[test]
28375    fn parser_recognises_wait_for_wal_position_with_timeout() {
28376        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28377        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28378            panic!()
28379        };
28380        assert_eq!(pos, 67890);
28381        assert_eq!(timeout_ms, Some(5000));
28382    }
28383
28384    #[test]
28385    fn parser_rejects_wait_with_negative_position() {
28386        // The lexer treats `-` as a token; `expect_u64_literal`
28387        // only sees the Integer that follows, so the negative
28388        // arrives as a unary-minus expression at higher levels.
28389        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28390        // parse error one way or another.
28391        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28392        assert!(!err.message.is_empty());
28393    }
28394
28395    #[test]
28396    fn parser_recognises_bare_analyze() {
28397        let s = parse("ANALYZE");
28398        assert!(matches!(s, Statement::Analyze(None)));
28399    }
28400
28401    #[test]
28402    fn parser_recognises_analyze_with_table() {
28403        let s = parse("ANALYZE users");
28404        let Statement::Analyze(Some(name)) = s else {
28405            panic!("expected Analyze, got {s:?}")
28406        };
28407        assert_eq!(name, "users");
28408    }
28409
28410    #[test]
28411    fn parser_recognises_analyze_with_quoted_table() {
28412        let s = parse("ANALYZE \"Mixed Case\"");
28413        let Statement::Analyze(Some(name)) = s else {
28414            panic!()
28415        };
28416        assert_eq!(name, "Mixed Case");
28417    }
28418
28419    #[test]
28420    fn parser_rejects_analyze_with_garbage_token() {
28421        let err = parse_statement("ANALYZE 42").expect_err("must error");
28422        assert!(!err.message.is_empty());
28423    }
28424
28425    #[test]
28426    fn analyze_display_roundtrips() {
28427        for sql in ["ANALYZE", "ANALYZE users"] {
28428            let s = parse(sql);
28429            let printed = s.to_string();
28430            let again = parse_statement(&printed)
28431                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28432            assert_eq!(s, again);
28433        }
28434    }
28435
28436    #[test]
28437    fn wait_for_display_roundtrips() {
28438        for sql in [
28439            "WAIT FOR WAL POSITION 12345",
28440            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28441        ] {
28442            let s = parse(sql);
28443            let printed = s.to_string();
28444            let again = parse_statement(&printed)
28445                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28446            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28447        }
28448    }
28449
28450    #[test]
28451    fn subscription_ddl_display_roundtrips() {
28452        for sql in [
28453            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28454            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28455            "DROP SUBSCRIPTION sub_a",
28456            "SHOW SUBSCRIPTIONS",
28457        ] {
28458            let s = parse(sql);
28459            let printed = s.to_string();
28460            let again = parse_statement(&printed)
28461                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28462            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28463        }
28464    }
28465
28466    #[test]
28467    fn parser_drop_dispatches_user_vs_publication() {
28468        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28469        // tokenises DROP. Both targets must still parse.
28470        let s = parse("DROP USER 'alice'");
28471        let Statement::DropUser { name, .. } = s else {
28472            panic!("expected DropUser, got {s:?}")
28473        };
28474        assert_eq!(name, "alice");
28475        // And DROP PUBLICATION lands the new variant.
28476        let s = parse("DROP PUBLICATION p1");
28477        assert!(matches!(s, Statement::DropPublication { .. }));
28478    }
28479
28480    #[test]
28481    fn publication_ddl_display_roundtrips() {
28482        // Every CREATE PUBLICATION variant must Display → parse →
28483        // same AST. v6.1.3 covers all three scope shapes.
28484        for sql in [
28485            "CREATE PUBLICATION pub_a",
28486            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28487            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28488            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28489            "DROP PUBLICATION pub_a",
28490            "SHOW PUBLICATIONS",
28491        ] {
28492            let s = parse(sql);
28493            let printed = s.to_string();
28494            let again = parse_statement(&printed)
28495                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28496            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28497        }
28498    }
28499
28500    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28501
28502    #[test]
28503    fn create_function_returns_trigger_plpgsql_minimal() {
28504        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28505        let s = parse(sql);
28506        let Statement::CreateFunction(f) = s else {
28507            panic!("expected CreateFunction");
28508        };
28509        assert_eq!(f.name, "noop");
28510        assert!(!f.or_replace);
28511        assert!(f.args.is_empty());
28512        assert!(matches!(f.returns, FunctionReturn::Trigger));
28513        assert_eq!(f.language, "plpgsql");
28514        let FunctionBody::PlPgSql(block) = f.body else {
28515            panic!("expected PlPgSql body");
28516        };
28517        assert_eq!(block.statements.len(), 1);
28518        assert!(matches!(
28519            block.statements[0],
28520            PlPgSqlStmt::Return(ReturnTarget::New)
28521        ));
28522    }
28523
28524    #[test]
28525    fn create_function_or_replace_with_assignment() {
28526        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28527        // RETURN NEW.
28528        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28529BEGIN
28530  NEW.search_vector := to_tsvector('english', NEW.subject);
28531  RETURN NEW;
28532END;
28533$$";
28534        let s = parse(sql);
28535        let Statement::CreateFunction(f) = s else {
28536            panic!("expected CreateFunction");
28537        };
28538        assert!(f.or_replace);
28539        let FunctionBody::PlPgSql(block) = &f.body else {
28540            panic!("expected PlPgSql body");
28541        };
28542        assert_eq!(block.statements.len(), 2);
28543        // First statement: NEW.search_vector := to_tsvector(...)
28544        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28545            panic!("expected Assign as first stmt");
28546        };
28547        match target {
28548            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28549            other => panic!("expected NEW.col, got {other:?}"),
28550        }
28551        // Second statement: RETURN NEW
28552        assert!(matches!(
28553            block.statements[1],
28554            PlPgSqlStmt::Return(ReturnTarget::New)
28555        ));
28556    }
28557
28558    #[test]
28559    fn create_trigger_after_insert_or_update() {
28560        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28561        let s = parse(sql);
28562        let Statement::CreateTrigger(t) = s else {
28563            panic!("expected CreateTrigger");
28564        };
28565        assert_eq!(t.name, "tg");
28566        assert_eq!(t.table, "messages");
28567        assert_eq!(t.timing, TriggerTiming::After);
28568        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28569        assert_eq!(t.for_each, TriggerForEach::Row);
28570        assert_eq!(t.function, "update_sv");
28571    }
28572
28573    #[test]
28574    fn create_trigger_before_delete_execute_procedure_alias() {
28575        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28576        let sql =
28577            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28578        let s = parse(sql);
28579        let Statement::CreateTrigger(t) = s else {
28580            panic!("expected CreateTrigger");
28581        };
28582        assert_eq!(t.timing, TriggerTiming::Before);
28583        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28584    }
28585
28586    #[test]
28587    fn drop_trigger_if_exists_round_trips() {
28588        // No parser support for DROP TRIGGER yet — added in v7.12.5
28589        // alongside the broader DROP …{IF EXISTS} cleanup. The
28590        // AST + Display impls are in place so we round-trip via
28591        // construction:
28592        let s = Statement::DropTrigger {
28593            name: "tg".into(),
28594            table: "messages".into(),
28595            if_exists: true,
28596        };
28597        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28598    }
28599
28600    #[test]
28601    fn trigger_ddl_display_roundtrips_through_parser() {
28602        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28603        // Display → parse → same AST (modulo PL/pgSQL body
28604        // formatting which is parser-canonicalised).
28605        for sql in [
28606            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28607            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28608        ] {
28609            let s = parse(sql);
28610            let printed = s.to_string();
28611            let again = parse_statement(&printed)
28612                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28613            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28614        }
28615    }
28616}