Skip to main content

spg_sql/
parser.rs

1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21    AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22    CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23    CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24    ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25    FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26    FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27    GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28    NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29    PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30    Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31    WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38    Vec<Expr>,
39    Vec<(Expr, bool, Option<bool>)>,
40    Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50    if grant {
51        Statement::Grant(g)
52    } else {
53        Statement::Revoke(g)
54    }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58    matches!(
59        lc,
60        // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61        // real statement with a real store. v7.39 (read01 round 57): "grant" /
62        // "revoke" moved OUT — table privileges are now REAL (stored in
63        // `relacl`, enforced against the session role); a grant on any other
64        // object class still parses and no-ops so dumps restore.
65        // MySQL bulk-load brackets.
66        "unlock"
67            // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68            // diagnostics that pg_dump-style tools also emit
69            // post-restore.
70            | "optimize"
71            | "check"
72            | "use"
73            // PG psql backslash meta-commands that newer
74            // pg_dump versions emit unescaped (\restrict /
75            // \unrestrict). Real psql intercepts these; SPG's
76            // PG-wire sees them as raw text.
77            | "\\restrict"
78            | "\\unrestrict"
79            // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80            // `DELIMITER ;` directives. Technically client-side
81            // (the `mysql` CLI uses them to set the statement
82            // terminator), not SQL — but mysqldump and stored-
83            // procedure scripts emit them inline. SPG's parser
84            // sees one statement at a time and doesn't care
85            // about the terminator, so consume DELIMITER lines
86            // as Empty.
87            | "delimiter"
88            // v7.37.17 (17.6 siblings) — additional PG maintenance /
89            // session-state statements pg_dump + application startup
90            // scripts emit. SPG has no matching session-state to
91            // discard (no prepared-plan cache surface, no temp
92            // sequences), no matching security-label / storage-
93            // option to apply, no separate CREATE/DROP CAST that
94            // affects execution.
95            // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96            // pg_dump / pg_dumpall emit around DROP ROLE:
97            //   REASSIGN OWNED BY <role> [, ...] TO <newrole>
98            //   DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99            // Both operate on the role's owned objects; SPG has no
100            // role-owner model, so accept-and-no-op.
101            // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102            // + extension scripts use LOAD to preload shared
103            // libraries. SPG doesn't have a shared-library extension
104            // point today (extensions ship as first-class crates
105            // linked at build time); accept as a no-op.
106            | "load"
107    )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133    let s = match tok {
134        // PG keyword class: unreserved or col_name.
135        //
136        Token::Release => "release",
137        Token::Savepoint => "savepoint",
138        Token::Show => "show",
139        Token::Index => "index",
140        Token::Begin => "begin",
141        Token::Commit => "commit",
142        Token::Rollback => "rollback",
143        Token::Drop => "drop",
144        Token::Insert => "insert",
145        Token::Values => "values",
146        Token::Limit => "limit",
147        Token::Partition => "partition",
148        Token::Tables => "tables",
149        Token::Connection => "connection",
150        Token::Publication => "publication",
151        Token::Subscription => "subscription",
152        Token::Interval => "interval",
153        // `extract` is non-reserved in PG too (it's a function the
154        // parser dispatches via context — outside that context it's
155        // a plain identifier).
156        Token::Extract => "extract",
157        Token::Offset => "offset",
158        // `to` is reserved in PG (used in many "AS … TO …" forms), so
159        // it is NOT relaxed here. Same for `from`, `where`, `as`,
160        // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161        // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162        // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163        // `left`, `cross`, `outer`, `default`, `is`, `between`,
164        // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165        // (partial — keep partition as unreserved per modern PG).
166        _ => return None,
167    };
168    Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183    s.eq_ignore_ascii_case("jsonb_each_text")
184        || s.eq_ignore_ascii_case("jsonb_each")
185        || s.eq_ignore_ascii_case("json_each_text")
186        || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195    fname: &str,
196    args: Vec<Expr>,
197    names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199    let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200        "make_date" => &["year", "month", "day"],
201        "make_time" => &["hour", "min", "sec"],
202        "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203        "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204        other => {
205            return Err(alloc::format!(
206                "function {other}(...) does not support named arguments"
207            ));
208        }
209    };
210    let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211    let mut next_positional = 0usize;
212    for (arg, name) in args.into_iter().zip(names.iter()) {
213        let idx = match name {
214            Some(n) => params
215                .iter()
216                .position(|p| p.eq_ignore_ascii_case(n))
217                .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218            None => {
219                let i = next_positional;
220                next_positional += 1;
221                i
222            }
223        };
224        if idx >= slots.len() {
225            return Err(alloc::format!("too many arguments for {fname}(...)"));
226        }
227        if slots[idx].is_some() {
228            return Err(alloc::format!(
229                "argument \"{}\" specified more than once",
230                params[idx]
231            ));
232        }
233        slots[idx] = Some(arg);
234    }
235    Ok(slots
236        .into_iter()
237        .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238        .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248    /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249    Expanded(String),
250    /// Exponent pushes the value outside PG's numeric format
251    /// (more than 131072 integer digits or 16383 fractional digits).
252    Overflow,
253    /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254    NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262    let s = s.trim();
263    let Some(epos) = s.find(['e', 'E']) else {
264        return SciExpanded::NotScientific;
265    };
266    let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267    let Ok(exp) = exp_str.parse::<i64>() else {
268        return SciExpanded::NotScientific;
269    };
270    let (neg, mant) = match mant.strip_prefix('-') {
271        Some(r) => (true, r),
272        None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273    };
274    let (int_part, frac_part) = match mant.split_once('.') {
275        Some((i, f)) => (i, f),
276        None => (mant, ""),
277    };
278    if (int_part.is_empty() && frac_part.is_empty())
279        || !int_part.bytes().all(|b| b.is_ascii_digit())
280        || !frac_part.bytes().all(|b| b.is_ascii_digit())
281    {
282        return SciExpanded::NotScientific;
283    }
284    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285    digits.push_str(int_part);
286    digits.push_str(frac_part);
287    // Decimal point position within `digits` after applying the exponent.
288    let new_point = int_part.len() as i64 + exp;
289    // PG's numeric format: up to 131072 digits before the point, 16383 after.
290    if new_point > 131_072 {
291        return SciExpanded::Overflow;
292    }
293    if (digits.len() as i64 - new_point) > 16_383 {
294        return SciExpanded::Overflow;
295    }
296    let sign = if neg { "-" } else { "" };
297    let plain = if new_point <= 0 {
298        let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299        out.push_str("0.");
300        for _ in 0..(-new_point) {
301            out.push('0');
302        }
303        out.push_str(&digits);
304        out
305    } else if (new_point as usize) >= digits.len() {
306        let mut out = digits;
307        for _ in 0..(new_point as usize - out.len()) {
308            out.push('0');
309        }
310        out
311    } else {
312        let mut out = String::with_capacity(digits.len() + 1);
313        out.push_str(&digits[..new_point as usize]);
314        out.push('.');
315        out.push_str(&digits[new_point as usize..]);
316        out
317    };
318    SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327    let padded = if hex.len() % 2 == 1 {
328        alloc::format!("0{hex}")
329    } else {
330        hex.to_string()
331    };
332    Expr::Cast {
333        expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334            "\\x{padded}"
335        )))),
336        target: CastTarget::Named("bytea".to_string()),
337    }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345    let pad = (8 - bits.len() % 8) % 8;
346    let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347    let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348    for nibble in padded.as_bytes().chunks(4) {
349        let mut v = 0u8;
350        for &b in nibble {
351            v = (v << 1) | (b - b'0');
352        }
353        hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354    }
355    hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365    match parse_decimal_literal(&s) {
366        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368        // its exact value as a NumericBig.
369        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370        // v7.39 (read01 numeric.c) — expand the exponent form.
371        None => match expand_scientific_literal(&s) {
372            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374                None if plain
375                    .split_once('.')
376                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377                {
378                    Ok(Literal::NumericBig(plain))
379                }
380                None => s
381                    .parse::<f64>()
382                    .map(Literal::Float)
383                    .map_err(|_| format!("invalid numeric literal {s:?}")),
384            },
385            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386            SciExpanded::NotScientific => s
387                .parse::<f64>()
388                .map(Literal::Float)
389                .map_err(|_| format!("invalid numeric literal {s:?}")),
390        },
391    }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395    let (int_part, frac_part) = match s.split_once('.') {
396        Some((i, f)) => (i, f),
397        None => (s, ""),
398    };
399    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400    // places fell out of the numeric path here, which is why
401    // `pg_typeof(1e-256)` answered double precision and a plain
402    // 256-place decimal aborted the query in the big-decimal converter.
403    if frac_part.len() > u16::MAX as usize {
404        return None;
405    }
406    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407    digits.push_str(int_part);
408    digits.push_str(frac_part);
409    let mantissa: i128 = digits.parse().ok()?;
410    #[allow(clippy::cast_possible_truncation)]
411    Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418    s.eq_ignore_ascii_case("jsonb_to_recordset")
419        || s.eq_ignore_ascii_case("jsonb_to_record")
420        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421        // column-definition list desugars identically (the record base
422        // argument only carries the type; a non-NULL base's field
423        // defaults are a recorded delta).
424        || s.eq_ignore_ascii_case("json_populate_record")
425        || s.eq_ignore_ascii_case("jsonb_populate_record")
426        || s.eq_ignore_ascii_case("json_populate_recordset")
427        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428        || s.eq_ignore_ascii_case("json_to_recordset")
429        || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433    /// Whether what follows an identifier ends an index key, which is how
434    /// an operator class is told from anything else in that position.
435    fn opclass_position_follows(next: Option<&Token>) -> bool {
436        match next {
437            // `ASC` / `DESC` have their own tokens; matching them as
438            // identifiers named "asc" / "desc" — which the first version of
439            // this did — never fires, and `(c text_pattern_ops DESC)` (which
440            // PG18.4 accepts, verified) went on failing to parse.
441            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442            Some(Token::Ident(w)) => {
443                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444            }
445            _ => false,
446        }
447    }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451    let lc = name.to_ascii_lowercase();
452    matches!(
453        lc.as_str(),
454        "vector_cosine_ops"
455            | "vector_l2_ops"
456            | "vector_ip_ops"
457            | "halfvec_cosine_ops"
458            | "halfvec_l2_ops"
459            | "halfvec_ip_ops"
460            | "sq8_cosine_ops"
461            | "sq8_l2_ops"
462            | "sq8_ip_ops"
463            // pg_trgm — trigram operator class. SPG's GIN index
464            // already uses tsvector tokens; trigram-style LIKE
465            // pattern matching still routes through a sequential
466            // scan, but the opclass name is accepted so PG schemas
467            // load.
468            | "gin_trgm_ops"
469            | "gist_trgm_ops"
470            // PG built-in btree opclasses occasionally appear in
471            // pg_dump output for column types with multiple
472            // sort orders (text_pattern_ops, varchar_pattern_ops,
473            // bpchar_pattern_ops).
474            | "text_pattern_ops"
475            | "varchar_pattern_ops"
476            | "bpchar_pattern_ops"
477            | "int4_ops"
478            | "int8_ops"
479            | "text_ops"
480    )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485    pub message: String,
486    /// Index into the token stream where parsing tripped. Not a byte offset.
487    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488    /// field would grow every `Result<_, ParseError>` slot on the deeply
489    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490    /// 1-based char position is recovered on the cold error path by
491    /// [`syntax_error_position`], which re-tokenizes to map this token index.
492    pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497    /// with `parse error at token #N: `, which PG has no equivalent of:
498    /// the message bodies are already PG's verbatim (`LIMIT must not be
499    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500    /// prefix was SPG's internal token index leaking into every one of
501    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502    /// character position from it for the ErrorResponse `P`.
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        f.write_str(&self.message)
505    }
506}
507
508impl From<LexError> for ParseError {
509    fn from(e: LexError) -> Self {
510        Self {
511            message: format!("lex: {e}"),
512            token_pos: 0,
513        }
514    }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523    let (tokens, offsets) =
524        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525    let mut p = Parser::new(tokens);
526    let expr = p
527        .parse_expr(0)
528        .and_then(|e| p.expect_eof().map(|()| e))
529        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530    Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536    parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544        .map_err(|e| shape_lex_error(&e, input))?;
545    // The same session flag names the dialect for both the lexer and
546    // the type mapping.
547    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548    let stmt = (|| {
549        let stmt = p.parse_one_statement()?;
550        if matches!(p.peek(), Token::Semicolon) {
551            p.advance();
552        }
553        p.expect_eof()?;
554        Ok(stmt)
555    })()
556    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557    Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575        return e;
576    }
577    let message = match offending_lexeme(input, offsets, e.token_pos) {
578        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579        None => "syntax error at end of input".into(),
580    };
581    ParseError {
582        message,
583        token_pos: e.token_pos,
584    }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595    use lexer::LexErrorKind as K;
596    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597    let message = match &e.kind {
598        K::UnterminatedString => {
599            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600        }
601        K::UnterminatedQuotedIdent => {
602            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603        }
604        K::UnterminatedBlockComment => {
605            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606        }
607        // PG has no "unknown character" error of its own — the character
608        // is skipped and the parser reports the next token. SPG stops at
609        // the character itself and names it, which is the same shape.
610        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611        // The number-literal kinds already carry PG's `at or near` form.
612        other => alloc::format!(
613            "{}",
614            lexer::LexError {
615                kind: other.clone(),
616                pos: e.pos,
617            }
618        ),
619    };
620    ParseError {
621        message,
622        token_pos: 0,
623    }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630    let start = *offsets.get(token_pos)?;
631    if start >= input.len() {
632        return None;
633    }
634    let end = offsets
635        .get(token_pos + 1)
636        .copied()
637        .unwrap_or(input.len())
638        .min(input.len());
639    let seg = input.get(start..end)?.trim();
640    if seg.is_empty() {
641        return None;
642    }
643    // A quoted literal / identifier keeps its inner spaces; anything else
644    // ends at the first whitespace (the segment runs to the NEXT token's
645    // start, which may swallow a comment).
646    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647        Some(seg)
648    } else {
649        seg.split_whitespace().next()
650    }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663    input: &str,
664    backslash_escapes: bool,
665    token_pos: usize,
666) -> Option<usize> {
667    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668    let byte_off = *offsets.get(token_pos)?;
669    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670        return None;
671    }
672    Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676    tokens: Vec<Token>,
677    pos: usize,
678    /// v7.39 (round 274) — the session's dialect, carried by the same
679    /// signal that drives string-literal escaping: `SET sql_mode` (only
680    /// MySQL clients and mysqldump preambles emit it) turns it on,
681    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682    /// it off. Needed here because the two dialects disagree about what
683    /// `REAL` means — see the type mapping below.
684    mysql_dialect: bool,
685    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686    /// mutually recursive expr/select parsers. Bounded so a deeply
687    /// nested input returns a parse error instead of overflowing
688    /// the stack (embed hosts die on overflow — it is an abort,
689    /// not a catchable error).
690    nest_depth: usize,
691    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692    /// `random() < p/100` predicate here; the enclosing SELECT
693    /// drains the list after its WHERE parses and ANDs the
694    /// predicates in. parse_bare_select save/restores around its
695    /// FROM+WHERE so nested selects only drain their own.
696    pending_sample_preds: Vec<Expr>,
697    /// v7.39 (round 691) — collation lowering channel, the same shape as
698    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699    /// information, and `ast::OrderBy` is where this parser keeps ordering
700    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701    /// variant — puts a new arm on `eval_expr`, which this repo has
702    /// measured to overflow the debug stack. So while an ORDER BY KEY is
703    /// being parsed the postfix loop drops the name here instead of
704    /// refusing it, and the key's parser takes it.
705    ///
706    /// Only inside an ORDER BY key: everywhere else an unperformable
707    /// collation still errors, because accepting one at a COMPARISON and
708    /// ignoring it is the defect F36 exists to close.
709    in_order_by_key: bool,
710    order_key_collation: Option<String>,
711    /// POSITION(sub IN str) — while parsing the needle, the IN
712    /// keyword is the argument separator, not a membership test.
713    /// The postfix loop leaves IN unconsumed when this is set.
714    suppress_in_tail: bool,
715    /// Index of the token the last `advance()` returned — see
716    /// [`Parser::consumed_pos`].
717    last_consumed: usize,
718    /// v7.39 (round 506) — the statement's own text and the byte each token
719    /// starts at, so a MySQL projection item can report the SOURCE TEXT
720    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
721    /// spacing and all. Only filled for a MySQL session — a PG one names
722    /// columns from the parsed shape and pays nothing for this.
723    src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739///     this budget and errors. Verified against a live server for nested
740///     derived tables, parens, calls, CASE, IN-subqueries, scalar
741///     subqueries, NOT and unary minus — the server stayed up through all
742///     of them. This is the contract that matters, and it holds.
743///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745///     and executing aborts around 8 inside a test thread. The budget is
746///     simply unreachable there, which is why a deep-nesting test has to
747///     ask for a large stack of its own — see `nesting_budget_errors_at`
748///     in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767    "pg_am",
768    "pg_attrdef",
769    "pg_attribute",
770    "pg_cast",
771    "pg_db_role_setting",
772    "pg_conversion",
773    "pg_default_acl",
774    "pg_shadow",
775    "pg_sequences",
776    "pg_range",
777    "pg_partitioned_table",
778    "pg_language",
779    "pg_group",
780    "pg_authid",
781    "pg_class",
782    "pg_collation",
783    "pg_constraint",
784    "pg_database",
785    "pg_depend",
786    "pg_amop",
787    "pg_amproc",
788    "pg_opclass",
789    "pg_opfamily",
790    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
791    "pg_description",
792    "pg_enum",
793    "pg_extension",
794    // v7.39 (round 541) — pg_dump reads it for every relation of kind
795    // 'f'. SPG has no foreign tables, so it is empty, which is also
796    // what PG reports on a database that has none.
797    "pg_foreign_table",
798    // v7.39 (round 541) — the empty-by-truth family; see
799    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
800    "pg_event_trigger",
801    "pg_file_settings",
802    "pg_foreign_data_wrapper",
803    "pg_foreign_server",
804    "pg_hba_file_rules",
805    "pg_ident_file_mappings",
806    "pg_init_privs",
807    "pg_parameter_acl",
808    "pg_prepared_xacts",
809    "pg_publication_namespace",
810    "pg_publication_rel",
811    "pg_publication_tables",
812    "pg_replication_origin",
813    "pg_replication_origin_status",
814    "pg_seclabel",
815    "pg_seclabels",
816    "pg_shdepend",
817    "pg_shdescription",
818    "pg_shmem_allocations",
819    "pg_shmem_allocations_numa",
820    "pg_shseclabel",
821    "pg_statistic_ext_data",
822    "pg_stats_ext",
823    "pg_stats_ext_exprs",
824    "pg_subscription_rel",
825    "pg_transform",
826    "pg_user_mapping",
827    "pg_user_mappings",
828    "pg_index",
829    "pg_indexes",
830    "pg_inherits",
831    // v7.39 (round 650) — the text-search catalogs SPG can fill
832    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
833    // token types to dictionaries and SPG has no token-type model,
834    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
835    "pg_ts_config",
836    "pg_ts_config_map",
837    "pg_ts_dict",
838    "pg_ts_parser",
839    "pg_ts_template",
840    "pg_matviews",
841    "pg_namespace",
842    // v7.39 (round 621)
843    "pg_operator",
844    "pg_policies",
845    "pg_policy",
846    "pg_proc",
847    "pg_publication",
848    "pg_replication_slots",
849    "pg_roles",
850    // v7.39 (round 143) — the rewrite-rule listing view.
851    // v7.39 (round 312) — and the rule catalogue itself, which
852    // `pg_get_ruledef(oid)` resolves against.
853    "pg_rewrite",
854    "pg_rules",
855    "pg_sequence",
856    "pg_settings",
857    "pg_stat_archiver",
858    "pg_stat_bgwriter",
859    "pg_stat_checkpointer",
860    "pg_stat_database",
861    "pg_stat_io",
862    "pg_stat_progress_analyze",
863    "pg_auth_members",
864    "pg_stat_progress_create_index",
865    "pg_stat_progress_vacuum",
866    "pg_stat_replication",
867    "pg_stat_slru",
868    "pg_stat_subscription_stats",
869    "pg_stat_user_functions",
870    "pg_stat_user_indexes",
871    "pg_stat_user_tables",
872    "pg_stat_wal",
873    "pg_prepared_statements",
874    "pg_largeobject",
875    "pg_largeobject_metadata",
876    "pg_statistic",
877    "pg_statistic_ext",
878    // v7.38.18 — the readable view over pg_statistic.
879    "pg_stats",
880    "pg_subscription",
881    "pg_tables",
882    "pg_tablespace",
883    // v7.39 (round 502) — the timezone catalogues. SPG resolved
884    // named zones correctly but could not list them, so a client
885    // populating a timezone picker got "relation does not exist".
886    "pg_timezone_abbrevs",
887    "pg_timezone_names",
888    "pg_trigger",
889    "pg_type",
890    "pg_user",
891    "pg_views",
892];
893
894const MAX_NEST_DEPTH: usize = 64;
895
896/// Stack accounting for the nesting budget, test-only.
897///
898/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
899/// that MOVES: a compiler upgrade grew the parser's debug frames and
900/// silently ate the margin until `nesting_budget_errors_cleanly` went
901/// from erroring cleanly to aborting on a stack overflow. A count
902/// cannot notice that on its own, so the budget is measured here and
903/// held to a ceiling.
904///
905/// The reading has to come from a helper whose OWN frame is the same at
906/// every call: debug slot placement does not follow source order, so a
907/// local's address inside the function under test is not that
908/// function's frame boundary. Two earlier probes were wrong that way —
909/// one read `&self.nest_depth`, which is the `Parser`'s address and
910/// never moves at all.
911#[cfg(test)]
912mod frame_meter {
913    extern crate std;
914    use std::cell::Cell;
915
916    // Per-THREAD, not global. `cargo test` runs tests in parallel and
917    // plenty of them parse nested expressions, so shared statics get
918    // stack addresses from several threads at once and the subtraction
919    // below turns into noise — it read 229,772 bytes per level that way,
920    // while passing when the test was run on its own.
921    std::thread_local! {
922        static AT_LO: Cell<usize> = const { Cell::new(0) };
923        static AT_HI: Cell<usize> = const { Cell::new(0) };
924    }
925
926    pub(super) const SAMPLE_LO: usize = 4;
927    pub(super) const SAMPLE_HI: usize = 24;
928
929    #[inline(never)]
930    pub(super) fn record(depth: usize) {
931        let anchor = 0u8;
932        let at = core::ptr::from_ref(&anchor) as usize;
933        if depth == SAMPLE_LO {
934            AT_LO.with(|c| c.set(at));
935        } else if depth == SAMPLE_HI {
936            AT_HI.with(|c| c.set(at));
937        }
938    }
939
940    /// Bytes of stack one nesting level costs, averaged over the span.
941    pub(super) fn bytes_per_level() -> usize {
942        let lo = AT_LO.with(Cell::get);
943        let hi = AT_HI.with(Cell::get);
944        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
945        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
946        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
947    }
948
949    pub(super) fn reset() {
950        AT_LO.with(|c| c.set(0));
951        AT_HI.with(|c| c.set(0));
952    }
953}
954
955/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
956/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
957#[inline(never)]
958fn build_center_call(e: Expr) -> Expr {
959    Expr::FunctionCall {
960        name: alloc::string::String::from("center"),
961        args: alloc::vec![e],
962    }
963}
964
965/// Max consecutive binary operators at ONE precedence level
966/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
967/// parse time but evaluates and drops recursively — depth beyond
968/// this overflows 2 MiB worker stacks (debug eval frames run
969/// multiple KiB). `IN (…)` lists are flat and unaffected.
970const MAX_BINARY_CHAIN: usize = 256;
971
972/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
973/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
974/// it keeps its dedicated path (`parse_table_level_fk`).
975enum NamedTableConstraintKind {
976    Check,
977    Unique,
978    PrimaryKey,
979    Exclude,
980}
981
982impl Parser {
983    fn new(tokens: Vec<Token>) -> Self {
984        Self::new_with_dialect(tokens, false)
985    }
986
987    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
988        Self {
989            tokens,
990            mysql_dialect,
991            in_order_by_key: false,
992            order_key_collation: None,
993            pos: 0,
994            nest_depth: 0,
995            pending_sample_preds: Vec::new(),
996            suppress_in_tail: false,
997            last_consumed: 0,
998            src: None,
999        }
1000    }
1001
1002    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1003    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1004        if self.mysql_dialect {
1005            self.src = Some((input.to_string(), offsets.to_vec()));
1006        }
1007        self
1008    }
1009
1010    /// The source text spanning tokens `start ..= end`, trimmed.
1011    ///
1012    /// The offsets are token STARTS, so the span runs to the start of the
1013    /// token after `end` and gives back the whitespace between them —
1014    /// trimming is what makes `a + b FROM t` end at `b`.
1015    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1016        let (text, offsets) = self.src.as_ref()?;
1017        let from = *offsets.get(start)?;
1018        let to = *offsets.get(end + 1)?;
1019        text.get(from..to).map(str::trim_end)
1020    }
1021
1022    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1023    /// nesting depth, erroring out cleanly past the budget.
1024    fn enter_nested(&mut self) -> Result<(), ParseError> {
1025        self.nest_depth += 1;
1026        #[cfg(test)]
1027        frame_meter::record(self.nest_depth);
1028        if self.nest_depth > MAX_NEST_DEPTH {
1029            self.nest_depth -= 1;
1030            return Err(self.err(alloc::format!(
1031                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1032            )));
1033        }
1034        Ok(())
1035    }
1036
1037    fn peek(&self) -> &Token {
1038        // tokens always ends with Eof; pos is clamped in advance().
1039        &self.tokens[self.pos]
1040    }
1041
1042    fn advance(&mut self) -> Token {
1043        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1044        self.last_consumed = self.pos;
1045        if self.pos + 1 < self.tokens.len() {
1046            self.pos += 1;
1047        }
1048        t
1049    }
1050
1051    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1052    /// returned. It was computed as `pos - 1`, which is wrong at both
1053    /// ends: `advance()` parks on the final Eof rather than running off
1054    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1055    /// input`), and after backtracking `pos` is no longer one past the
1056    /// token that failed. Recorded by `advance()` itself instead.
1057    const fn consumed_pos(&self) -> usize {
1058        self.last_consumed
1059    }
1060
1061    fn err(&self, message: String) -> ParseError {
1062        ParseError {
1063            message,
1064            token_pos: self.pos,
1065        }
1066    }
1067
1068    fn expect_eof(&self) -> Result<(), ParseError> {
1069        if matches!(self.peek(), Token::Eof) {
1070            Ok(())
1071        } else {
1072            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1073        }
1074    }
1075
1076    /// v7.14.0 — swallow every token up to (but not including) the
1077    /// next semicolon / EOF. Used by the dump-noise dispatcher
1078    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1079    /// etc. without modeling each grammar.
1080    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1081    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1082    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1083    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1084    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1085    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1086        let start = self.pos;
1087        self.advance(); // COMMENT
1088        if !matches!(self.peek(), Token::On) {
1089            self.pos = start;
1090            self.consume_until_statement_boundary();
1091            return Ok(Statement::Empty);
1092        }
1093        self.advance(); // ON
1094        let kind = match self.peek() {
1095            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1096            Token::Table => "table".into(),
1097            _ => {
1098                self.consume_until_statement_boundary();
1099                return Ok(Statement::Empty);
1100            }
1101        };
1102        if !matches!(
1103            kind.as_str(),
1104            "table"
1105                | "column"
1106                | "index"
1107                | "view"
1108                | "sequence"
1109                | "schema"
1110                | "type"
1111                | "database"
1112                | "function"
1113        ) {
1114            self.consume_until_statement_boundary();
1115            return Ok(Statement::Empty);
1116        }
1117        self.advance(); // the kind keyword
1118        // The object name. ⚠️ `expect_ident_like` strips a leading
1119        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1120        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1121        // `c`. Read the dotted parts from raw tokens instead, then let a
1122        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1123        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1124        loop {
1125            match self.advance() {
1126                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1127                other if unreserved_keyword_text(&other).is_some() => {
1128                    parts.push(unreserved_keyword_text(&other).unwrap());
1129                }
1130                other => {
1131                    return Err(ParseError {
1132                        message: alloc::format!("expected identifier, got {other:?}"),
1133                        token_pos: self.consumed_pos(),
1134                    });
1135                }
1136            }
1137            if matches!(self.peek(), Token::Dot) {
1138                self.advance();
1139            } else {
1140                break;
1141            }
1142        }
1143        // COLUMN wants `table.column`; every other kind wants a bare name.
1144        let want = if kind == "column" { 2 } else { 1 };
1145        while parts.len() > want {
1146            parts.remove(0);
1147        }
1148        let name = parts.join(".");
1149        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1150        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1151        // error here — a dump carrying one function comment failed to
1152        // restore. The list is consumed (the comment store keys by name;
1153        // overload-precise comments are the function-predicate follow-up).
1154        if matches!(self.peek(), Token::LParen)
1155            && matches!(
1156                kind.as_str(),
1157                "function" | "procedure" | "aggregate" | "routine"
1158            )
1159        {
1160            let mut depth = 0usize;
1161            loop {
1162                match self.advance() {
1163                    Token::LParen => depth += 1,
1164                    Token::RParen => {
1165                        depth -= 1;
1166                        if depth == 0 {
1167                            break;
1168                        }
1169                    }
1170                    Token::Eof => {
1171                        return Err(self.err(alloc::string::String::from(
1172                            "unterminated argument list in COMMENT ON",
1173                        )));
1174                    }
1175                    _ => {}
1176                }
1177            }
1178        }
1179        // `IS`
1180        if !matches!(self.peek(), Token::Is) {
1181            self.expect_keyword_ident("is")?;
1182        } else {
1183            self.advance();
1184        }
1185        let comment = match self.peek() {
1186            Token::Null => {
1187                self.advance();
1188                None
1189            }
1190            _ => Some(self.expect_string_literal()?),
1191        };
1192        Ok(Statement::CommentOn {
1193            kind,
1194            name,
1195            comment,
1196        })
1197    }
1198
1199    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1200    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1201    /// [CASCADE|RESTRICT]`.
1202    ///
1203    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1204    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1205    /// and the no-ON `GRANT role TO role` membership form — parses into
1206    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1207    /// on them still restores.
1208    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1209        self.advance(); // GRANT / REVOKE
1210        // REVOKE's optional `GRANT OPTION FOR` prefix.
1211        let mut grant_option = false;
1212        if !grant
1213            && self.peek_keyword_ident("grant")
1214            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1215        {
1216            self.advance(); // GRANT
1217            self.advance(); // OPTION
1218            self.expect_keyword_ident("for")?;
1219            grant_option = true;
1220        }
1221        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1222        // words each with an optional COLUMN list.
1223        let mut privileges: Vec<GrantPriv> = Vec::new();
1224        if matches!(self.peek(), Token::All) {
1225            self.advance();
1226            if self.peek_keyword_ident("privileges") {
1227                self.advance();
1228            }
1229            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1230            // column only.
1231            if matches!(self.peek(), Token::LParen) {
1232                let columns = self.parse_grant_column_list()?;
1233                privileges.push(GrantPriv {
1234                    word: "ALL".into(),
1235                    columns,
1236                });
1237            }
1238        } else {
1239            loop {
1240                // SELECT and INSERT lex as reserved tokens, so they never
1241                // reach `expect_ident_like` as plain idents; the rest
1242                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1243                // MAINTAIN) are ordinary identifiers.
1244                let w = match self.peek() {
1245                    Token::Select => {
1246                        self.advance();
1247                        "SELECT".to_string()
1248                    }
1249                    Token::Insert => {
1250                        self.advance();
1251                        "INSERT".to_string()
1252                    }
1253                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1254                    // schema / database, and it lexes as a reserved token.
1255                    Token::Create => {
1256                        self.advance();
1257                        "CREATE".to_string()
1258                    }
1259                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1260                    // alice`) these "privilege words" are ROLE NAMES, and a
1261                    // role name is case-sensitive. `priv_from_word` folds case
1262                    // itself when they really are privileges.
1263                    _ => self.expect_ident_like()?,
1264                };
1265                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1266                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1267                let columns = if matches!(self.peek(), Token::LParen) {
1268                    self.parse_grant_column_list()?
1269                } else {
1270                    Vec::new()
1271                };
1272                privileges.push(GrantPriv { word: w, columns });
1273                if matches!(self.peek(), Token::Comma) {
1274                    self.advance();
1275                } else {
1276                    break;
1277                }
1278            }
1279        }
1280        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1281        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1282        if !matches!(self.peek(), Token::On) {
1283            let roles: Vec<String> = core::mem::take(&mut privileges)
1284                .into_iter()
1285                .map(|p| p.word)
1286                .collect();
1287            let grantees = self.parse_grantee_list(grant)?;
1288            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1289            // no admin-option layer: a member cannot re-grant).
1290            self.consume_until_statement_boundary();
1291            return Ok(finish_grant(
1292                grant,
1293                GrantStatement {
1294                    privileges: Vec::new(),
1295                    object: GrantObject::Roles(roles),
1296                    grantees,
1297                    grant_option,
1298                },
1299            ));
1300        }
1301        self.advance(); // ON
1302        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1303        // the enforced case; anything else parses and no-ops.
1304        let mut class = "TABLE";
1305        match self.peek() {
1306            Token::Table => {
1307                self.advance();
1308            }
1309            Token::All => {
1310                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1311                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1312                // IN SCHEMA` stay no-ops and keep their own object class.
1313                self.advance(); // ALL
1314                let kind = match self.peek() {
1315                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1316                    // TABLES has its own token (SHOW TABLES owns it).
1317                    Token::Tables | Token::Table => "tables".to_string(),
1318                    _ => String::new(),
1319                };
1320                if !kind.is_empty() {
1321                    self.advance();
1322                }
1323                // `IN SCHEMA <name>`
1324                if matches!(self.peek(), Token::In) {
1325                    self.advance();
1326                    if self.peek_keyword_ident("schema") {
1327                        self.advance();
1328                        let _schema = self.expect_ident_like()?;
1329                    }
1330                }
1331                if kind != "tables" {
1332                    self.consume_until_statement_boundary();
1333                    return Ok(finish_grant(
1334                        grant,
1335                        GrantStatement {
1336                            privileges,
1337                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1338                            grantees: Vec::new(),
1339                            grant_option,
1340                        },
1341                    ));
1342                }
1343                let grantees = self.parse_grantee_list(grant)?;
1344                self.consume_until_statement_boundary();
1345                return Ok(finish_grant(
1346                    grant,
1347                    GrantStatement {
1348                        privileges,
1349                        object: GrantObject::AllTablesInSchema,
1350                        grantees,
1351                        grant_option,
1352                    },
1353                ));
1354            }
1355            Token::Ident(w) | Token::QuotedIdent(w) => {
1356                let lc = w.to_ascii_lowercase();
1357                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1358                // real objects with real ACLs now.
1359                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1360                    self.advance();
1361                    let mut names: Vec<String> = Vec::new();
1362                    loop {
1363                        let mut parts: Vec<String> = Vec::new();
1364                        loop {
1365                            parts.push(self.expect_ident_like()?);
1366                            if matches!(self.peek(), Token::Dot) {
1367                                self.advance();
1368                            } else {
1369                                break;
1370                            }
1371                        }
1372                        names.push(parts.pop().expect("at least one part"));
1373                        if matches!(self.peek(), Token::Comma) {
1374                            self.advance();
1375                        } else {
1376                            break;
1377                        }
1378                    }
1379                    let grantees = self.parse_grantee_list(grant)?;
1380                    let mut grant_option = grant_option;
1381                    if grant && self.peek_keyword_ident("with") {
1382                        self.advance();
1383                        self.expect_keyword_ident("grant")?;
1384                        self.expect_keyword_ident("option")?;
1385                        grant_option = true;
1386                    }
1387                    self.consume_until_statement_boundary();
1388                    let object = match lc.as_str() {
1389                        "sequence" => GrantObject::Sequences(names),
1390                        "schema" => GrantObject::Schemas(names),
1391                        _ => GrantObject::Databases(names),
1392                    };
1393                    return Ok(finish_grant(
1394                        grant,
1395                        GrantStatement {
1396                            privileges,
1397                            object,
1398                            grantees,
1399                            grant_option,
1400                        },
1401                    ));
1402                }
1403                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1404                // keys functions by NAME, so the argument list parses and is
1405                // dropped (an overload set shares one ACL — recorded residual).
1406                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1407                    self.advance();
1408                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1409                    loop {
1410                        let mut parts: Vec<String> = Vec::new();
1411                        loop {
1412                            parts.push(self.expect_ident_like()?);
1413                            if matches!(self.peek(), Token::Dot) {
1414                                self.advance();
1415                            } else {
1416                                break;
1417                            }
1418                        }
1419                        let fname = parts.pop().expect("at least one part");
1420                        // v7.39 (read01 round 62) — the signature picks the
1421                        // overload, so it is captured.
1422                        let sig = if matches!(self.peek(), Token::LParen) {
1423                            Some(self.parse_function_signature_types()?)
1424                        } else {
1425                            None
1426                        };
1427                        names.push((fname, sig));
1428                        if matches!(self.peek(), Token::Comma) {
1429                            self.advance();
1430                        } else {
1431                            break;
1432                        }
1433                    }
1434                    let grantees = self.parse_grantee_list(grant)?;
1435                    self.consume_until_statement_boundary();
1436                    return Ok(finish_grant(
1437                        grant,
1438                        GrantStatement {
1439                            privileges,
1440                            object: GrantObject::Functions(names),
1441                            grantees,
1442                            grant_option,
1443                        },
1444                    ));
1445                }
1446                if matches!(
1447                    lc.as_str(),
1448                    "type"
1449                        | "domain"
1450                        | "language"
1451                        | "tablespace"
1452                        | "large"
1453                        | "foreign"
1454                        | "parameter"
1455                ) {
1456                    self.consume_until_statement_boundary();
1457                    return Ok(finish_grant(
1458                        grant,
1459                        GrantStatement {
1460                            privileges,
1461                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1462                            grantees: Vec::new(),
1463                            grant_option,
1464                        },
1465                    ));
1466                }
1467                class = "TABLE";
1468            }
1469            _ => {}
1470        }
1471        let _ = class;
1472        // The table list. Schema-qualified names drop their qualifier (SPG is
1473        // single-schema) — but read the dotted parts from raw tokens, since
1474        // `expect_ident_like` would silently swallow the leading part.
1475        let mut tables: Vec<String> = Vec::new();
1476        loop {
1477            let mut parts: Vec<String> = Vec::new();
1478            loop {
1479                parts.push(self.expect_ident_like()?);
1480                if matches!(self.peek(), Token::Dot) {
1481                    self.advance();
1482                } else {
1483                    break;
1484                }
1485            }
1486            tables.push(parts.pop().expect("at least one part"));
1487            if matches!(self.peek(), Token::Comma) {
1488                self.advance();
1489            } else {
1490                break;
1491            }
1492        }
1493        let grantees = self.parse_grantee_list(grant)?;
1494        if grant && self.peek_keyword_ident("with") {
1495            self.advance();
1496            self.expect_keyword_ident("grant")?;
1497            self.expect_keyword_ident("option")?;
1498            grant_option = true;
1499        }
1500        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1501        // to cascade to (no re-granting), so both are accepted and ignored.
1502        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1503            self.advance();
1504        }
1505        Ok(finish_grant(
1506            grant,
1507            GrantStatement {
1508                privileges,
1509                object: GrantObject::Tables(tables),
1510                grantees,
1511                grant_option,
1512            },
1513        ))
1514    }
1515
1516    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1517    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1518    /// words; the caller normalises them into a signature key.
1519    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1520        self.advance(); // (
1521        let mut types: Vec<String> = Vec::new();
1522        if matches!(self.peek(), Token::RParen) {
1523            self.advance();
1524            return Ok(types);
1525        }
1526        loop {
1527            // Collect the words of one argument up to a comma / close paren.
1528            let mut words: Vec<String> = Vec::new();
1529            loop {
1530                match self.peek() {
1531                    Token::Comma | Token::RParen | Token::Eof => break,
1532                    _ => {}
1533                }
1534                let tok = self.advance();
1535                match tok {
1536                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1537                    other => {
1538                        if let Some(w) = unreserved_keyword_text(&other) {
1539                            words.push(w);
1540                        }
1541                    }
1542                }
1543            }
1544            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1545            // themselves several words (`double precision`, `character
1546            // varying`, `timestamp with time zone`), so "two words means the
1547            // first is a parameter name" reads the type off `f(double
1548            // precision)` as `precision`. v7.39 (round 282): recognise the
1549            // multi-word spellings first — a leading word that STARTS one of
1550            // them is part of the type, not a name.
1551            let joined = words.join(" ");
1552            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1553                joined
1554            } else if words.len() >= 2 {
1555                words[1..].join(" ")
1556            } else {
1557                words.first().cloned().unwrap_or_default()
1558            };
1559            types.push(ty);
1560            if matches!(self.peek(), Token::Comma) {
1561                self.advance();
1562            } else {
1563                break;
1564            }
1565        }
1566        if matches!(self.peek(), Token::RParen) {
1567            self.advance();
1568        }
1569        Ok(types)
1570    }
1571
1572    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1573    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1574        self.advance(); // (
1575        let mut cols = Vec::new();
1576        loop {
1577            cols.push(self.expect_ident_like()?);
1578            if matches!(self.peek(), Token::Comma) {
1579                self.advance();
1580            } else {
1581                break;
1582            }
1583        }
1584        if !matches!(self.peek(), Token::RParen) {
1585            return Err(self.err(alloc::format!(
1586                "expected ')' to close the column list, got {:?}",
1587                self.peek()
1588            )));
1589        }
1590        self.advance(); // )
1591        Ok(cols)
1592    }
1593
1594    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1595    /// PUBLIC.
1596    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1597        if grant {
1598            if matches!(self.peek(), Token::To) {
1599                self.advance();
1600            } else {
1601                self.expect_keyword_ident("to")?;
1602            }
1603        } else if matches!(self.peek(), Token::From) {
1604            self.advance();
1605        } else {
1606            self.expect_keyword_ident("from")?;
1607        }
1608        let mut grantees: Vec<String> = Vec::new();
1609        loop {
1610            // `GROUP name` is the legacy spelling of a plain role name.
1611            if self.peek_keyword_ident("group") {
1612                self.advance();
1613            }
1614            if self.peek_keyword_ident("public") {
1615                self.advance();
1616                grantees.push(String::new()); // PUBLIC
1617            } else {
1618                grantees.push(self.expect_ident_like()?);
1619            }
1620            if matches!(self.peek(), Token::Comma) {
1621                self.advance();
1622            } else {
1623                break;
1624            }
1625        }
1626        Ok(grantees)
1627    }
1628
1629    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1630    /// The body keeps its `$N` placeholders; substitution happens at
1631    /// EXECUTE. The declared types are recorded for
1632    /// `pg_prepared_statements.parameter_types` but are not enforced —
1633    /// PG infers when the list is omitted, and SPG resolves the values
1634    /// at substitution time either way.
1635    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1636        let start = self.pos;
1637        self.advance(); // PREPARE
1638        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1639        // different statement that happens to share the keyword. PG
1640        // ships with `max_prepared_transactions = 0` and reports it
1641        // this way; SPG has no prepared-transaction registry, so the
1642        // same wording is the accurate answer rather than a dodge.
1643        // Round 277 turned this from a silent no-op into a confusing
1644        // "expected AS in PREPARE" parse error.
1645        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1646            self.advance();
1647            let gid = match self.advance() {
1648                Token::String(g) => g,
1649                other => {
1650                    return Err(self.err(alloc::format!(
1651                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1652                    )));
1653                }
1654            };
1655            return Ok(Statement::PrepareTransaction(gid));
1656        }
1657        let name = self.expect_ident_like()?;
1658        let mut param_types = Vec::new();
1659        if matches!(self.peek(), Token::LParen) {
1660            self.advance();
1661            loop {
1662                let mut ty = self.expect_ident_like()?;
1663                // A parameterised type name (`numeric(10,2)`,
1664                // `varchar(20)`) keeps its argument list in the text.
1665                if matches!(self.peek(), Token::LParen) {
1666                    let mut depth = 0usize;
1667                    let mut buf = String::from("(");
1668                    loop {
1669                        match self.advance() {
1670                            Token::LParen => {
1671                                depth += 1;
1672                                if depth > 1 {
1673                                    buf.push('(');
1674                                }
1675                            }
1676                            Token::RParen => {
1677                                depth -= 1;
1678                                buf.push(')');
1679                                if depth == 0 {
1680                                    break;
1681                                }
1682                            }
1683                            Token::Comma => buf.push(','),
1684                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1685                            Token::Eof => break,
1686                            _ => {}
1687                        }
1688                    }
1689                    ty.push_str(&buf);
1690                }
1691                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1692                // position, same family as the parameter list above.
1693                let array_suffix = self.consume_array_suffix();
1694                ty.push_str(&array_suffix);
1695                param_types.push(ty);
1696                match self.peek() {
1697                    Token::Comma => {
1698                        self.advance();
1699                    }
1700                    Token::RParen => {
1701                        self.advance();
1702                        break;
1703                    }
1704                    other => {
1705                        return Err(self.err(alloc::format!(
1706                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1707                        )));
1708                    }
1709                }
1710            }
1711        }
1712        if !matches!(self.peek(), Token::As) {
1713            return Err(self.err(alloc::format!(
1714                "expected AS in PREPARE, got {:?}",
1715                self.peek()
1716            )));
1717        }
1718        self.advance();
1719        let body = self.parse_one_statement()?;
1720        // The Parser holds tokens, not the source text, so the
1721        // statement PG reports in `pg_prepared_statements.statement`
1722        // is rebuilt from the AST rather than sliced from the input.
1723        let _ = start;
1724        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1725        if !param_types.is_empty() {
1726            source.push_str(" (");
1727            source.push_str(&param_types.join(", "));
1728            source.push(')');
1729        }
1730        source.push_str(" AS ");
1731        source.push_str(&alloc::format!("{body}"));
1732        Ok(Statement::Prepare {
1733            name,
1734            param_types,
1735            body: alloc::boxed::Box::new(body),
1736            source,
1737        })
1738    }
1739
1740    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1741    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1742        self.advance(); // EXECUTE
1743        let name = self.expect_ident_like()?;
1744        let mut args = Vec::new();
1745        if matches!(self.peek(), Token::LParen) {
1746            self.advance();
1747            if matches!(self.peek(), Token::RParen) {
1748                self.advance();
1749            } else {
1750                loop {
1751                    args.push(self.parse_expr(0)?);
1752                    match self.advance() {
1753                        Token::Comma => {}
1754                        Token::RParen => break,
1755                        other => {
1756                            return Err(self.err(alloc::format!(
1757                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1758                            )));
1759                        }
1760                    }
1761                }
1762            }
1763        }
1764        Ok(Statement::Execute { name, args })
1765    }
1766
1767    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1768    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1769    /// procedure catalog yet, so this reports PG's not-found error
1770    /// (with its HINT) rather than pretending the call ran.
1771    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1772    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1773    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1774        self.advance(); // DISCARD
1775        let target = match self.advance() {
1776            Token::All => DiscardTarget::All,
1777            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1778                "all" => DiscardTarget::All,
1779                "plans" => DiscardTarget::Plans,
1780                "sequences" => DiscardTarget::Sequences,
1781                "temp" | "temporary" => DiscardTarget::Temp,
1782                other => {
1783                    return Err(self.err(format!(
1784                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785                    )));
1786                }
1787            },
1788            other => {
1789                return Err(self.err(format!(
1790                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1791                )));
1792            }
1793        };
1794        Ok(Statement::Discard(target))
1795    }
1796
1797    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1798    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1799    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1800    /// aggressively the server interrupts, which SPG does not distinguish.
1801    /// Bare `KILL <id>` means CONNECTION.
1802    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1803        self.advance(); // KILL
1804        let mut query_only = false;
1805        loop {
1806            // CONNECTION is a reserved keyword token (it also opens
1807            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1808            // `Token::Connection` rather than a bare ident.
1809            if matches!(self.peek(), Token::Connection) {
1810                self.advance();
1811                break;
1812            }
1813            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1814                break;
1815            };
1816            match w.to_ascii_lowercase().as_str() {
1817                "hard" | "soft" => {
1818                    self.advance();
1819                }
1820                "query" => {
1821                    self.advance();
1822                    query_only = true;
1823                    break;
1824                }
1825                _ => break,
1826            }
1827        }
1828        let id = self.parse_expr(0)?;
1829        Ok(Statement::Kill {
1830            query_only,
1831            id: Box::new(id),
1832        })
1833    }
1834
1835    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1836        self.advance(); // CALL
1837        let name = self.expect_ident_like()?;
1838        self.consume_until_statement_boundary();
1839        Ok(Statement::Call(name))
1840    }
1841
1842    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1843        self.advance(); // DEALLOCATE
1844        // PG accepts an optional noise `PREPARE` keyword here.
1845        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1846            self.advance();
1847        }
1848        if matches!(self.peek(), Token::All) {
1849            self.advance();
1850            return Ok(Statement::Deallocate(None));
1851        }
1852        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1853            self.advance();
1854            return Ok(Statement::Deallocate(None));
1855        }
1856        let name = self.expect_ident_like()?;
1857        Ok(Statement::Deallocate(Some(name)))
1858    }
1859
1860    fn consume_until_statement_boundary(&mut self) {
1861        loop {
1862            match self.peek() {
1863                Token::Semicolon | Token::Eof => return,
1864                _ => self.advance(),
1865            };
1866        }
1867    }
1868
1869    /// v7.38.18 — consume to the statement boundary like
1870    /// `consume_until_statement_boundary`, but pick out the collation a
1871    /// `CREATE DATABASE` asked for on the way.
1872    ///
1873    /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1874    /// `LC_CTYPE` does not, because SPG has no separate ctype and
1875    /// pretending to honour it would be the more misleading answer. An
1876    /// `=` between the keyword and the value is optional, as in PG.
1877    ///
1878    /// The whole statement used to be thrown away. Being single-database
1879    /// makes the NAME a no-op; it does not make the collation one.
1880    fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1881        let mut want_value = false;
1882        let mut found: Option<String> = None;
1883        loop {
1884            let tok = self.peek().clone();
1885            match &tok {
1886                Token::Semicolon | Token::Eof => break,
1887                Token::Ident(w) | Token::QuotedIdent(w)
1888                    if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1889                {
1890                    want_value = true;
1891                }
1892                Token::Eq if want_value => {}
1893                Token::String(v) if want_value => {
1894                    found = Some(v.clone());
1895                    want_value = false;
1896                }
1897                Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1898                    found = Some(v.clone());
1899                    want_value = false;
1900                }
1901                _ => want_value = false,
1902            }
1903            self.advance();
1904        }
1905        found
1906    }
1907
1908    /// v7.22 (round-13 T2) — consume to the statement boundary like
1909    /// `consume_until_statement_boundary`, but pick out the sequence
1910    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1911    /// columns) or the first string literal (`nextval('<seq>')`).
1912    /// Schema qualifiers and `::regclass` casts are stripped.
1913    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1914        let mut seq: Option<String> = None;
1915        let mut after_sequence_kw = false;
1916        let mut after_name_kw = false;
1917        loop {
1918            match self.peek().clone() {
1919                Token::Semicolon | Token::Eof => break,
1920                Token::Ident(s) | Token::QuotedIdent(s) => {
1921                    if after_name_kw && seq.is_none() {
1922                        self.advance();
1923                        let mut name = s;
1924                        // `SEQUENCE NAME public.groups_id_seq` — keep
1925                        // the bare name, drop qualifiers.
1926                        while matches!(self.peek(), Token::Dot) {
1927                            self.advance();
1928                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1929                                name = n;
1930                            }
1931                        }
1932                        seq = Some(name);
1933                        after_name_kw = false;
1934                        continue;
1935                    }
1936                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1937                        after_name_kw = true;
1938                        after_sequence_kw = false;
1939                    } else {
1940                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1941                    }
1942                    self.advance();
1943                }
1944                Token::String(s) => {
1945                    if seq.is_none() {
1946                        // `nextval('public.groups_id_seq'::regclass)`
1947                        let bare = s
1948                            .rsplit_once('.')
1949                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1950                        seq = Some(bare);
1951                    }
1952                    self.advance();
1953                }
1954                _ => {
1955                    after_sequence_kw = false;
1956                    after_name_kw = false;
1957                    self.advance();
1958                }
1959            }
1960        }
1961        seq
1962    }
1963
1964    /// v7.39 (round 621) — is the next token the keyword `BY`?
1965    ///
1966    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1967    /// column, table and alias name — and SPG lexed it into a dedicated
1968    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1969    /// two-letter keywords the lexer knew, this was the only one PG leaves
1970    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1971    ///
1972    /// The token is gone; the three clauses that own the word — GROUP BY,
1973    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1974    /// ask this instead. Adding it to the unreserved-identifier table was not
1975    /// enough on its own: identifier positions that match the token shape
1976    /// directly (an index's column list, a table alias) never consult that
1977    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1978    /// Not lexing it as a keyword closes the whole class rather than the two
1979    /// positions that happened to be noticed.
1980    fn peek_is_by(&self) -> bool {
1981        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1982    }
1983
1984    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1985    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1986    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1987    fn consume_drop_behaviour(&mut self) {
1988        if matches!(
1989            self.peek(),
1990            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1991        ) {
1992            self.advance();
1993        }
1994    }
1995
1996    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1997        let first = match self.advance() {
1998            Token::Ident(s) | Token::QuotedIdent(s) => s,
1999            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2000            // per PG's `pg_get_keywords()` classification. SPG tokenizes
2001            // these as named variants for parsing leverage in the
2002            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2003            // `BEGIN`, etc.), but they MUST still be usable as table /
2004            // column / alias names in DDL+DML. Sentori migrations like
2005            // 0001_init.sql ship `release TEXT NOT NULL` in the events
2006            // table — the `events.release` column carries the release
2007            // identifier string. Pre-T4 this triggered "expected
2008            // identifier, got Release" and blocked every drop-in user
2009            // whose schema had a column / alias with one of these names.
2010            other if unreserved_keyword_text(&other).is_some() => {
2011                unreserved_keyword_text(&other).unwrap()
2012            }
2013            other => {
2014                return Err(ParseError {
2015                    message: format!("expected identifier, got {other:?}"),
2016                    token_pos: self.consumed_pos(),
2017                });
2018            }
2019        };
2020        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2021        // qualify every name with `public.` (and pg_catalog.* for
2022        // functions); SPG is single-schema so we discard the
2023        // prefix and return only the trailing ident. Same shape
2024        // also handles MySQL `db.tbl` cross-database refs (SPG
2025        // ignores the db part).
2026        if matches!(self.peek(), Token::Dot) {
2027            self.advance();
2028            match self.advance() {
2029                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2030                other if unreserved_keyword_text(&other).is_some() => {
2031                    return Ok(unreserved_keyword_text(&other).unwrap());
2032                }
2033                other => {
2034                    return Err(ParseError {
2035                        message: format!("expected identifier after '{first}.', got {other:?}"),
2036                        token_pos: self.consumed_pos(),
2037                    });
2038                }
2039            }
2040        }
2041        Ok(first)
2042    }
2043
2044    #[allow(clippy::too_many_lines)]
2045    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2046        // v7.14.0 — empty / comment-only / semicolon-only input
2047        // (after the lexer strips line + block + MySQL
2048        // conditional comments) lands as Statement::Empty.
2049        // pg_dump and mysqldump emit several wrappers that
2050        // collapse to nothing after stripping (`/*!40101 SET …
2051        // */;`, blank lines between statements); the engine
2052        // returns CommandOk no-op so the dump loads cleanly.
2053        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2054            return Ok(Statement::Empty);
2055        }
2056        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2057        // catalog / metadata DDL that has no behavioural effect
2058        // on SPG's single-schema, single-database, single-user
2059        // model. Consume the whole statement up to the next
2060        // semicolon / EOF and return Empty. This is broader than
2061        // the per-keyword DROP / SET / COMMENT arms but lets the
2062        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2063        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2064        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2065        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2066            let lc = s.to_ascii_lowercase();
2067            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2068            if lc == "comment" {
2069                return self.parse_comment_on();
2070            }
2071            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2072            if lc == "grant" || lc == "revoke" {
2073                return self.parse_grant_or_revoke(lc == "grant");
2074            }
2075            // v7.39 (round 277) — the SQL-level prepared-statement
2076            // surface is REAL now. It used to be accepted and dropped
2077            // on the theory that "real execution still happens via the
2078            // extended-query flow" — true only for a driver that uses
2079            // that flow; a plain SQL PREPARE / EXECUTE returned no
2080            // rows at all.
2081            if lc == "prepare" {
2082                return self.parse_prepare();
2083            }
2084            if lc == "execute" {
2085                return self.parse_execute();
2086            }
2087            if lc == "deallocate" {
2088                return self.parse_deallocate();
2089            }
2090            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2091            // accepted and dropped, so an application's stored-procedure
2092            // invocation reported success and did nothing. SPG has no
2093            // procedure catalog, so every CALL names a procedure that
2094            // does not exist — which is exactly what PG says.
2095            if lc == "call" {
2096                return self.parse_call();
2097            }
2098            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2099            // names one connection and acts on it.
2100            if lc == "kill" {
2101                return self.parse_kill();
2102            }
2103            if lc == "discard" {
2104                return self.parse_discard();
2105            }
2106            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2107            // Still performs nothing; the roles are carried out so a name
2108            // that does not exist is refused, as PG18 refuses it.
2109            if lc == "reassign" {
2110                self.advance();
2111                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2112                    self.advance();
2113                }
2114                if self.peek_is_by() {
2115                    self.advance();
2116                }
2117                // Only the roles BEFORE the TO are the ones that must
2118                // exist — `TO` names the new owner, which PG checks as
2119                // well, so both lists are collected.
2120                let mut names = self.take_comma_separated_names();
2121                if matches!(self.peek(), Token::To) {
2122                    self.advance();
2123                    names.extend(self.take_comma_separated_names());
2124                }
2125                self.consume_until_statement_boundary();
2126                return Ok(Statement::ValidateOnly {
2127                    kind: crate::ast::ValidateOnlyKind::RoleName,
2128                    names,
2129                });
2130            }
2131            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2132            // unconditionally with `no security label providers have been
2133            // loaded`, whatever object it names, because none is loaded.
2134            // SPG has none either; accepting it told the caller a label had
2135            // been applied when nothing anywhere records one.
2136            if lc == "security" {
2137                self.consume_until_statement_boundary();
2138                return Ok(Statement::ValidateOnly {
2139                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2140                    names: Vec::new(),
2141                });
2142            }
2143            if is_dump_noise_statement(&lc) {
2144                self.consume_until_statement_boundary();
2145                return Ok(Statement::Empty);
2146            }
2147        }
2148        match self.peek() {
2149            Token::Select => self.parse_select_stmt(),
2150            // v7.37.17 (17.6 siblings) — a statement opening with a
2151            // parenthesized query group: `(SELECT … UNION …)
2152            // INTERSECT …`. parse_bare_select's group arm consumes
2153            // the parens; the select parser handles the outer chain
2154            // and tail.
2155            Token::LParen
2156                if matches!(
2157                    self.tokens.get(self.pos + 1),
2158                    Some(Token::Select | Token::LParen | Token::Values)
2159                ) =>
2160            {
2161                self.parse_select_stmt()
2162            }
2163            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2164            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2165            // Lowers to the same UNION ALL chain the FROM-position
2166            // form uses, then reuses the shared SELECT tail.
2167            Token::Values => {
2168                self.advance(); // VALUES
2169                let mut head = self.parse_values_rows_body()?;
2170                self.parse_select_tail_into(&mut head)?;
2171                Ok(Statement::Select(head))
2172            }
2173            // SQL-standard `TABLE name` shorthand for
2174            // `SELECT * FROM name` — pg_dump never emits it, but
2175            // psql users and PG docs use it constantly. Set-op
2176            // chains and the ORDER BY/LIMIT tail compose like any
2177            // SELECT head.
2178            Token::Table
2179                if matches!(
2180                    self.tokens.get(self.pos + 1),
2181                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2182                ) =>
2183            {
2184                let mut head = self.parse_table_shorthand()?;
2185                self.parse_setop_chain_into(&mut head)?;
2186                self.parse_select_tail_into(&mut head)?;
2187                Ok(Statement::Select(head))
2188            }
2189            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2190            // body is a dollar-quoted plpgsql block (lexer already
2191            // collapsed `$$…$$` into a single Token::String).
2192            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2193            // real PlPgSqlBlock so the engine can EXECUTE it at
2194            // top level instead of silently swallowing. Pre-
2195            // v7.16.2 the parser threw the body away and the
2196            // engine returned CommandOk for the entire DO; that
2197            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2198            // $$` into a SEV-1 silent no-op (the IF + the rename
2199            // were both invisible — mailrs's migrate-042 didn't
2200            // actually run). Now the body parses + executes;
2201            // EmbeddedSql inside the block runs immediately
2202            // against the engine (not deferred — we're at top
2203            // level, not inside a trigger row-write loop).
2204            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2205                self.advance();
2206                let body_text = match self.advance() {
2207                    Token::String(s) => s,
2208                    other => {
2209                        return Err(self.err(alloc::format!(
2210                            "expected dollar-quoted body after DO, got {other:?}"
2211                        )));
2212                    }
2213                };
2214                // Optional `LANGUAGE <name>` trailer (idents only).
2215                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2216                    self.advance();
2217                    let _ = self.expect_ident_like()?;
2218                }
2219                // Parse the body — same shape CREATE FUNCTION
2220                // uses for trigger function bodies. If the body
2221                // doesn't parse cleanly we surface the error
2222                // (better than silent no-op).
2223                let block = parse_plpgsql_body(&body_text)?;
2224                Ok(Statement::DoBlock(block))
2225            }
2226            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2227            // WITH isn't a reserved token in our lexer — comes through
2228            // as `Token::Ident("with")` (case-insensitive).
2229            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2230                self.advance();
2231                self.parse_with_cte_then_select()
2232            }
2233            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2234            // an identifier — not a reserved keyword.
2235            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2236                self.advance();
2237                let mut analyze = false;
2238                let mut suggest = false;
2239                let mut costs_off = false;
2240                let mut buffers = false;
2241                let mut timing_off = false;
2242                let mut settings = false;
2243                let mut wal = false;
2244                let mut summary_off = false;
2245                let mut format = crate::ast::ExplainFormat::Text;
2246                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2247                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2248                // options are comma-separated. Booleans default to ON
2249                // when the value token is omitted (matches PG).
2250                if matches!(self.peek(), Token::LParen) {
2251                    self.advance();
2252                    loop {
2253                        let opt = match self.peek().clone() {
2254                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2255                            other => {
2256                                return Err(self.err(format!(
2257                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2258                                )));
2259                            }
2260                        };
2261                        self.advance();
2262                        if opt.eq_ignore_ascii_case("suggest") {
2263                            suggest = true;
2264                            // SUGGEST takes no explicit value today.
2265                        } else if opt.eq_ignore_ascii_case("costs") {
2266                            // PG syntax: `COSTS [ON | OFF]`. Default
2267                            // when value omitted is ON, so plain
2268                            // `COSTS` is a no-op. `COSTS OFF` flips.
2269                            // `ON` lexes to `Token::On` (reserved
2270                            // keyword in JOIN ... ON contexts); accept
2271                            // it alongside the bare Ident form so the
2272                            // grammar matches PG verbatim.
2273                            let value = match self.peek().clone() {
2274                                Token::On => {
2275                                    self.advance();
2276                                    true
2277                                }
2278                                Token::Ident(v) | Token::QuotedIdent(v)
2279                                    if v.eq_ignore_ascii_case("off") =>
2280                                {
2281                                    self.advance();
2282                                    false
2283                                }
2284                                Token::Ident(v) | Token::QuotedIdent(v)
2285                                    if v.eq_ignore_ascii_case("true") =>
2286                                {
2287                                    self.advance();
2288                                    true
2289                                }
2290                                _ => true,
2291                            };
2292                            costs_off = !value;
2293                        } else if opt.eq_ignore_ascii_case("analyze")
2294                            || opt.eq_ignore_ascii_case("analyse")
2295                        {
2296                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2297                            // Same default-ON rule as ANALYZE keyword form.
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                            analyze = value;
2318                        } else if opt.eq_ignore_ascii_case("buffers") {
2319                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2320                            let value = match self.peek().clone() {
2321                                Token::On => {
2322                                    self.advance();
2323                                    true
2324                                }
2325                                Token::Ident(v) | Token::QuotedIdent(v)
2326                                    if v.eq_ignore_ascii_case("off") =>
2327                                {
2328                                    self.advance();
2329                                    false
2330                                }
2331                                Token::Ident(v) | Token::QuotedIdent(v)
2332                                    if v.eq_ignore_ascii_case("true") =>
2333                                {
2334                                    self.advance();
2335                                    true
2336                                }
2337                                _ => true,
2338                            };
2339                            buffers = value;
2340                        } else if opt.eq_ignore_ascii_case("timing") {
2341                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2342                            // the measured wall-clock annotation.
2343                            let value = match self.peek().clone() {
2344                                Token::On => {
2345                                    self.advance();
2346                                    true
2347                                }
2348                                Token::Ident(v) | Token::QuotedIdent(v)
2349                                    if v.eq_ignore_ascii_case("off") =>
2350                                {
2351                                    self.advance();
2352                                    false
2353                                }
2354                                Token::Ident(v) | Token::QuotedIdent(v)
2355                                    if v.eq_ignore_ascii_case("true") =>
2356                                {
2357                                    self.advance();
2358                                    true
2359                                }
2360                                _ => true,
2361                            };
2362                            timing_off = !value;
2363                        } else if opt.eq_ignore_ascii_case("settings") {
2364                            settings = true;
2365                        } else if opt.eq_ignore_ascii_case("wal") {
2366                            wal = true;
2367                        } else if opt.eq_ignore_ascii_case("summary") {
2368                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2369                            // gates the trailing Planning/Execution Time
2370                            // lines now (was accept-and-no-op).
2371                            let value = match self.peek().clone() {
2372                                Token::On => {
2373                                    self.advance();
2374                                    true
2375                                }
2376                                Token::Ident(v) | Token::QuotedIdent(v)
2377                                    if v.eq_ignore_ascii_case("off") =>
2378                                {
2379                                    self.advance();
2380                                    false
2381                                }
2382                                Token::Ident(v) | Token::QuotedIdent(v)
2383                                    if v.eq_ignore_ascii_case("true") =>
2384                                {
2385                                    self.advance();
2386                                    true
2387                                }
2388                                _ => true,
2389                            };
2390                            summary_off = !value;
2391                        } else if opt.eq_ignore_ascii_case("verbose")
2392                            || opt.eq_ignore_ascii_case("format")
2393                        {
2394                            // v7.37.22 — accept-but-no-op the remaining
2395                            // PG options so EXPLAIN-using clients
2396                            // (pgAdmin / DataGrip) don't see syntax
2397                            // errors. FORMAT takes a value (text /
2398                            // json / yaml / xml); skip the next token
2399                            // if it's an ident.
2400                            if opt.eq_ignore_ascii_case("format") {
2401                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2402                                {
2403                                    self.advance();
2404                                    format = match v.to_ascii_lowercase().as_str() {
2405                                        "text" => crate::ast::ExplainFormat::Text,
2406                                        "json" => crate::ast::ExplainFormat::Json,
2407                                        "xml" => crate::ast::ExplainFormat::Xml,
2408                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2409                                        other => {
2410                                            return Err(self.err(format!(
2411                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2412                                                 supports text, json, xml, yaml"
2413                                            )));
2414                                        }
2415                                    };
2416                                }
2417                            } else {
2418                                // VERBOSE / SUMMARY take optional ON/OFF;
2419                                // consume if present.
2420                                if matches!(self.peek(), Token::On) {
2421                                    self.advance();
2422                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2423                                    self.peek().clone()
2424                                    && (v.eq_ignore_ascii_case("off")
2425                                        || v.eq_ignore_ascii_case("true"))
2426                                {
2427                                    self.advance();
2428                                    let _ = v;
2429                                }
2430                            }
2431                        } else {
2432                            return Err(self.err(format!(
2433                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2434                            )));
2435                        }
2436                        if matches!(self.peek(), Token::Comma) {
2437                            self.advance();
2438                            continue;
2439                        }
2440                        break;
2441                    }
2442                    if !matches!(self.peek(), Token::RParen) {
2443                        return Err(self.err(format!(
2444                            "expected ')' after EXPLAIN options, got {:?}",
2445                            self.peek()
2446                        )));
2447                    }
2448                    self.advance();
2449                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2450                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2451                {
2452                    self.advance();
2453                    analyze = true;
2454                }
2455                // v7.39 (round 224) — the body may open with WITH (CTEs);
2456                // route through the same CTE-then-SELECT path the top-level
2457                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2458                // too (PG explains INSERT / UPDATE / DELETE).
2459                let inner = match self.peek().clone() {
2460                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2461                        self.advance();
2462                        self.parse_with_cte_then_select()?
2463                    }
2464                    Token::Insert => self.parse_insert_stmt(false)?,
2465                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2466                        self.advance();
2467                        self.parse_update_after_keyword()?
2468                    }
2469                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2470                        self.advance();
2471                        self.parse_delete_after_keyword()?
2472                    }
2473                    _ => self.parse_select_stmt()?,
2474                };
2475                if !matches!(
2476                    inner,
2477                    Statement::Select(_)
2478                        | Statement::Insert(_)
2479                        | Statement::Update(_)
2480                        | Statement::Delete(_)
2481                ) {
2482                    return Err(self.err(format!(
2483                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2484                    )));
2485                }
2486                Ok(Statement::Explain(crate::ast::ExplainStatement {
2487                    analyze,
2488                    inner: Box::new(inner),
2489                    suggest,
2490                    costs_off,
2491                    buffers,
2492                    timing_off,
2493                    settings,
2494                    wal,
2495                    summary_off,
2496                    format,
2497                }))
2498            }
2499            Token::Create => self.parse_create_stmt(),
2500            Token::Insert => self.parse_insert_stmt(false),
2501            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2502            // spelling; route to the same handler. DESC is the
2503            // reserved ORDER BY token, so it gets its own arm.
2504            Token::Ident(s)
2505                if s.eq_ignore_ascii_case("describe")
2506                    && matches!(
2507                        self.tokens.get(self.pos + 1),
2508                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2509                    ) =>
2510            {
2511                self.advance();
2512                let table = self.expect_ident_like()?;
2513                Ok(Statement::ShowColumns(table))
2514            }
2515            Token::Desc
2516                if matches!(
2517                    self.tokens.get(self.pos + 1),
2518                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2519                ) =>
2520            {
2521                self.advance();
2522                let table = self.expect_ident_like()?;
2523                Ok(Statement::ShowColumns(table))
2524            }
2525            // `COPY table [(cols)] TO STDOUT` — the export half of
2526            // pg_dump's COPY pair (the FROM stdin half rides the
2527            // embed import path). Options need a format design and
2528            // error honestly.
2529            Token::Ident(s)
2530                if s.eq_ignore_ascii_case("copy")
2531                    && matches!(
2532                        self.tokens.get(self.pos + 1),
2533                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2534                    ) =>
2535            {
2536                self.advance(); // COPY
2537                let table = self.expect_ident_like()?;
2538                let columns = if matches!(self.peek(), Token::LParen) {
2539                    self.advance();
2540                    let mut cols = alloc::vec![self.expect_ident_like()?];
2541                    while matches!(self.peek(), Token::Comma) {
2542                        self.advance();
2543                        cols.push(self.expect_ident_like()?);
2544                    }
2545                    if !matches!(self.peek(), Token::RParen) {
2546                        return Err(self.err(format!(
2547                            "expected ')' after COPY column list, got {:?}",
2548                            self.peek()
2549                        )));
2550                    }
2551                    self.advance();
2552                    Some(cols)
2553                } else {
2554                    None
2555                };
2556                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2557                // endpoint. (FROM STDIN still rides the wire/import path —
2558                // its data arrives out of band.)
2559                if matches!(self.peek(), Token::From)
2560                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2561                {
2562                    self.advance(); // FROM
2563                    let Token::String(path) = self.advance() else {
2564                        unreachable!()
2565                    };
2566                    let options = self.parse_copy_to_options()?;
2567                    return Ok(Statement::CopyFromFile {
2568                        table,
2569                        columns,
2570                        path,
2571                        options,
2572                    });
2573                }
2574                if !matches!(self.peek(), Token::To) {
2575                    return Err(self.err(format!(
2576                        "COPY: only TO STDOUT is supported here (FROM stdin \
2577                         rides the import path); got {:?}",
2578                        self.peek()
2579                    )));
2580                }
2581                self.advance();
2582                if matches!(self.peek(), Token::String(_)) {
2583                    let Token::String(path) = self.advance() else { unreachable!() };
2584                    let options = self.parse_copy_to_options()?;
2585                    return Ok(Statement::CopyToFile {
2586                        table,
2587                        columns,
2588                        query: None,
2589                        path,
2590                        options,
2591                    });
2592                }
2593                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2594                    return Err(self.err(format!(
2595                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2596                        self.peek()
2597                    )));
2598                }
2599                self.advance();
2600                let options = self.parse_copy_to_options()?;
2601                Ok(Statement::CopyTo {
2602                    table,
2603                    columns,
2604                    query: None,
2605                    options,
2606                })
2607            }
2608            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2609            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2610            // result set is streamed in COPY format (PG's query form).
2611            Token::Ident(s)
2612                if s.eq_ignore_ascii_case("copy")
2613                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2614            {
2615                self.advance(); // COPY
2616                self.advance(); // (
2617                let query = self.parse_select_stmt()?;
2618                if !matches!(self.peek(), Token::RParen) {
2619                    return Err(self.err(format!(
2620                        "expected ')' after COPY query, got {:?}",
2621                        self.peek()
2622                    )));
2623                }
2624                self.advance(); // )
2625                if !matches!(self.peek(), Token::To) {
2626                    return Err(self.err(format!(
2627                        "COPY (query): only TO STDOUT is supported, got {:?}",
2628                        self.peek()
2629                    )));
2630                }
2631                self.advance();
2632                if matches!(self.peek(), Token::String(_)) {
2633                    let Token::String(path) = self.advance() else { unreachable!() };
2634                    let options = self.parse_copy_to_options()?;
2635                    return Ok(Statement::CopyToFile {
2636                        table: String::new(),
2637                        columns: None,
2638                        query: Some(alloc::boxed::Box::new(query)),
2639                        path,
2640                        options,
2641                    });
2642                }
2643                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2644                    return Err(self.err(format!(
2645                        "COPY (query): TO supports STDOUT only, got {:?}",
2646                        self.peek()
2647                    )));
2648                }
2649                self.advance();
2650                let options = self.parse_copy_to_options()?;
2651                Ok(Statement::CopyTo {
2652                    table: String::new(),
2653                    columns: None,
2654                    query: Some(alloc::boxed::Box::new(query)),
2655                    options,
2656                })
2657            }
2658            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2659            // Shares the INSERT body; the replace flag lowers it
2660            // onto ON CONFLICT DO UPDATE with an empty assignment
2661            // list (engine: replace the whole row).
2662            Token::Ident(s)
2663                if s.eq_ignore_ascii_case("replace")
2664                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2665            {
2666                self.parse_insert_stmt(true)
2667            }
2668            Token::Begin => {
2669                self.advance();
2670                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2671                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2672                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2673                // is consumed first, then the trailing modes — including the
2674                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2675                // WORK/TRANSACTION). The explicit level, when present, rides the
2676                // statement so `exec_begin` applies it for this transaction.
2677                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2678                {
2679                    self.advance();
2680                }
2681                let iso = self.parse_isolation_level_clauses()?;
2682                Ok(Statement::Begin(iso))
2683            }
2684            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2685            // for BEGIN. START is contextual in PG too; pattern-match
2686            // on the ident here. Iso clauses are parse-and-ignored,
2687            // same as BEGIN above.
2688            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2689                self.advance();
2690                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2691                {
2692                    return Err(self.err(alloc::format!(
2693                        "expected TRANSACTION after START, got {:?}",
2694                        self.peek()
2695                    )));
2696                }
2697                self.advance();
2698                let iso = self.parse_isolation_level_clauses()?;
2699                Ok(Statement::Begin(iso))
2700            }
2701            Token::Commit => {
2702                self.advance();
2703                // PG: `COMMIT [WORK | TRANSACTION]`.
2704                if let Token::Ident(w) = self.peek()
2705                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2706                {
2707                    self.advance();
2708                }
2709                Ok(Statement::Commit)
2710            }
2711            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2712            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2713            // every transaction with `END;` and the drop-in aborted on
2714            // it. Only reachable at statement start (CASE … END lives
2715            // inside expressions), so no ambiguity.
2716            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2717                self.advance();
2718                if let Token::Ident(w) = self.peek()
2719                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2720                {
2721                    self.advance();
2722                }
2723                Ok(Statement::Commit)
2724            }
2725            Token::Rollback => {
2726                self.advance();
2727                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2728                // savepoint without ending the transaction. Bare
2729                // `ROLLBACK` drops the whole TX.
2730                if matches!(self.peek(), Token::To) {
2731                    self.advance();
2732                    if matches!(self.peek(), Token::Savepoint) {
2733                        self.advance();
2734                    }
2735                    let name = self.expect_ident_like()?;
2736                    Ok(Statement::RollbackToSavepoint(name))
2737                } else {
2738                    Ok(Statement::Rollback)
2739                }
2740            }
2741            Token::Savepoint => {
2742                self.advance();
2743                let name = self.expect_ident_like()?;
2744                Ok(Statement::Savepoint(name))
2745            }
2746            Token::Release => {
2747                self.advance();
2748                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2749                // is optional in standard SQL.
2750                if matches!(self.peek(), Token::Savepoint) {
2751                    self.advance();
2752                }
2753                let name = self.expect_ident_like()?;
2754                Ok(Statement::ReleaseSavepoint(name))
2755            }
2756            Token::Show => {
2757                self.advance();
2758                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2759                // v6.1.2 promoted TABLES to a reserved keyword (for
2760                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2761                // arrives as `Token::Tables` rather than a bare ident.
2762                // USERS / COLUMNS remain bare idents.
2763                let target = match self.advance() {
2764                    Token::Tables => "tables".to_string(),
2765                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2766                    // keyword token; recognise it as the SHOW CREATE
2767                    // dispatch keyword too.
2768                    Token::Create => "create".to_string(),
2769                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2770                    // keyword too; let SHOW INDEX FROM parse.
2771                    Token::Index => "index".to_string(),
2772                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2773                    // reserved (used in aggregate function calls);
2774                    // recognise it here so the parser dispatches
2775                    // to ShowParameter("all") — the engine returns
2776                    // the curated parameter inventory.
2777                    Token::All => "all".to_string(),
2778                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2779                    // spelling for the size of the diagnostics area.
2780                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2781                    // phrase with `syntax error at or near "("`, and a
2782                    // PG session must keep getting exactly that rather
2783                    // than a message about an unknown parameter.
2784                    // `COUNT` arrives as a bare ident; the `(*)` and the
2785                    // trailing keyword are consumed here so the whole
2786                    // form reaches the engine as one parameter name.
2787                    Token::Ident(ref c)
2788                        if self.mysql_dialect
2789                            && c.eq_ignore_ascii_case("count")
2790                            && matches!(self.peek(), Token::LParen) =>
2791                    {
2792                        self.advance();
2793                        if matches!(self.peek(), Token::Star) {
2794                            self.advance();
2795                        }
2796                        if matches!(self.peek(), Token::RParen) {
2797                            self.advance();
2798                        }
2799                        match self.advance() {
2800                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2801                                return Ok(Statement::ShowParameter(
2802                                    "count(*) warnings".to_string(),
2803                                ));
2804                            }
2805                            other => {
2806                                return Err(self.err(format!(
2807                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2808                                )));
2809                            }
2810                        }
2811                    }
2812                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2813                    other => {
2814                        return Err(self.err(format!(
2815                            "expected SHOW target, got {other:?}"
2816                        )));
2817                    }
2818                };
2819                match target.as_str() {
2820                    "tables" => Ok(Statement::ShowTables),
2821                    "users" => Ok(Statement::ShowUsers),
2822                    // v7.38 轴 4 — `SHOW transaction_isolation`
2823                    // returns the currently-selected isolation level.
2824                    "transaction_isolation" => Ok(Statement::ShowParameter(
2825                        "transaction_isolation".to_string(),
2826                    )),
2827                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2828                    // TABLE <t>` returns a 2-column row: (Table,
2829                    // Create Table). mysqldump emits this for every
2830                    // table at scrape time; without it the dump
2831                    // round-trip stalls.
2832                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2833                    // FROM <t>` (also spelled `SHOW INDEX` and
2834                    // `SHOW KEYS`). admin / mysqldump probes use
2835                    // it to list per-table indexes.
2836                    "indexes" | "index" | "keys" => {
2837                        if !matches!(self.peek(), Token::From) {
2838                            return Err(self.err(format!(
2839                                "expected FROM after SHOW INDEXES, got {:?}",
2840                                self.peek()
2841                            )));
2842                        }
2843                        self.advance();
2844                        let table = self.expect_ident_like()?;
2845                        Ok(Statement::ShowIndexes(table))
2846                    }
2847                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2848                    // `SHOW VARIABLES`. Both return a 2-column row
2849                    // set listing server-side state; clients probe
2850                    // them at connect time.
2851                    "status" => Ok(Statement::ShowStatus),
2852                    "variables" => {
2853                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2854                        if matches!(self.peek(), Token::Like) {
2855                            self.advance();
2856                            let pat = match self.advance() {
2857                                Token::String(p) => p,
2858                                other => {
2859                                    return Err(self.err(format!(
2860                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2861                                    )));
2862                                }
2863                            };
2864                            return Ok(Statement::ShowVariablesLike(pat));
2865                        }
2866                        Ok(Statement::ShowVariables)
2867                    }
2868                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2869                    "processlist" => Ok(Statement::ShowProcesslist),
2870                    "create" => {
2871                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2872                        // TABLE is supported in v7.17.
2873                        let kind = match self.advance() {
2874                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2875                            Token::Table => "table".to_string(),
2876                            other => {
2877                                return Err(self.err(format!(
2878                                    "expected TABLE after SHOW CREATE, got {other:?}"
2879                                )));
2880                            }
2881                        };
2882                        if !kind.eq_ignore_ascii_case("table") {
2883                            return Err(self.err(format!(
2884                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2885                            )));
2886                        }
2887                        let name = self.expect_ident_like()?;
2888                        Ok(Statement::ShowCreateTable(name))
2889                    }
2890                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2891                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2892                    // it to populate the database selector at connect
2893                    // time; without it `mysql -p` errors before the
2894                    // first user query.
2895                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2896                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2897                    // keyword on its own; it lands here as a bare
2898                    // ident. Returning all publications + their
2899                    // scope summary.
2900                    "publications" => Ok(Statement::ShowPublications),
2901                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2902                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2903                    "columns" => {
2904                        if !matches!(self.peek(), Token::From) {
2905                            return Err(self.err(format!(
2906                                "expected FROM after SHOW COLUMNS, got {:?}",
2907                                self.peek()
2908                            )));
2909                        }
2910                        self.advance();
2911                        let table = self.expect_ident_like()?;
2912                        Ok(Statement::ShowColumns(table))
2913                    }
2914                    // v7.38 轴 4 surface — `SHOW <param>` for any
2915                    // remaining session / preset parameter name
2916                    // (server_version, search_path, client_encoding,
2917                    // …). The engine's ShowParameter handler does the
2918                    // dispatch; unrecognised names error there with
2919                    // a pointer to pg_settings, not at parse time —
2920                    // so a driver that issues `SHOW spam_setting`
2921                    // gets a clear runtime error instead of a
2922                    // confusing "unknown SHOW target".
2923                    other => {
2924                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2925                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2926                        // consume the dotted tail so it round-trips with
2927                        // `SET app.foo` / `current_setting('app.foo')`.
2928                        let mut full = other.to_string();
2929                        while matches!(self.peek(), Token::Dot) {
2930                            self.advance();
2931                            let seg = self.expect_ident_like()?;
2932                            full.push('.');
2933                            full.push_str(&seg.to_ascii_lowercase());
2934                        }
2935                        Ok(Statement::ShowParameter(full))
2936                    }
2937                }
2938            }
2939            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2940            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2941            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2942            // arrived as a bare ident; tokenising it dedicatedly
2943            // keeps the dispatch tree small.
2944            Token::Drop => {
2945                self.advance();
2946                match self.peek() {
2947                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2948                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2949                    // around DROP ROLE cleanup. SPG has no role-owner
2950                    // model, so consume to boundary as a no-op.
2951                    Token::Ident(s) | Token::QuotedIdent(s)
2952                        if s.eq_ignore_ascii_case("owned") =>
2953                    {
2954                        // v7.39 (round 696) — still a no-op (SPG has no
2955                        // role-owner model), but the ROLE is carried out so
2956                        // the engine can refuse one that does not exist,
2957                        // which is what PG18 does.
2958                        self.advance();
2959                        if self.peek_is_by() {
2960                            self.advance();
2961                        }
2962                        let names = self.take_comma_separated_names();
2963                        self.consume_until_statement_boundary();
2964                        Ok(Statement::ValidateOnly {
2965                            kind: crate::ast::ValidateOnlyKind::RoleName,
2966                            names,
2967                        })
2968                    }
2969                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2970                    // It drops only a TEMPORARY table, and name resolution
2971                    // already prefers the session's own, so the keyword is
2972                    // consumed and the ordinary DROP TABLE path runs.
2973                    Token::Ident(s) | Token::QuotedIdent(s)
2974                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2975                    {
2976                        self.advance();
2977                        if !matches!(self.peek(), Token::Table) {
2978                            return Err(self.err(alloc::format!(
2979                                "expected TABLE after DROP TEMPORARY, got {:?}",
2980                                self.peek()
2981                            )));
2982                        }
2983                        self.parse_drop_table_after_keyword()
2984                    }
2985                    Token::Publication => {
2986                        self.advance();
2987                        // v7.39 (round 754, F31-B4) — the round-753
2988                        // audit probe tripped over the missing
2989                        // `IF EXISTS` here (syntax error).
2990                        let if_exists = self.consume_if_exists();
2991                        let name = self.expect_ident_or_string()?;
2992                        Ok(Statement::DropPublication { name, if_exists })
2993                    }
2994                    Token::Subscription => {
2995                        self.advance();
2996                        let if_exists = self.consume_if_exists();
2997                        let name = self.expect_ident_or_string()?;
2998                        Ok(Statement::DropSubscription { name, if_exists })
2999                    }
3000                    Token::Ident(s) | Token::QuotedIdent(s)
3001                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3002                    {
3003                        self.advance();
3004                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3005                        // login user IS a role in PG, and SPG's store holds
3006                        // both. `IF EXISTS` is accepted on either spelling.
3007                        let if_exists = self.consume_if_exists();
3008                        let name = self.expect_ident_or_string()?;
3009                        Ok(Statement::DropUser { name, if_exists })
3010                    }
3011                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3012                    // CREATE DATABASE has parsed since v7.14 and this did
3013                    // not, so `DROP DATABASE IF EXISTS x` — what every
3014                    // teardown script and pg_dumpall preamble opens with —
3015                    // came back as a syntax error, which IF EXISTS cannot
3016                    // soften. The name is carried so the engine can answer
3017                    // the way PG does; PG never lets this succeed on a
3018                    // single-database server, since the name is either
3019                    // unknown ("database … does not exist", or a notice
3020                    // under IF EXISTS) or the one you are connected to
3021                    // ("cannot drop the currently open database").
3022                    Token::Ident(s) | Token::QuotedIdent(s)
3023                        if s.eq_ignore_ascii_case("database") =>
3024                    {
3025                        self.advance();
3026                        let if_exists = self.consume_if_exists();
3027                        let name = self.expect_ident_or_string()?;
3028                        self.consume_until_statement_boundary();
3029                        Ok(Statement::DropDatabase { name, if_exists })
3030                    }
3031                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3032                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3033                        self.advance();
3034                        let if_exists = self.consume_if_exists();
3035                        let name = self.expect_ident_like()?;
3036                        // ON <table>
3037                        if !matches!(self.peek(), Token::On) {
3038                            return Err(self.err(alloc::format!(
3039                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3040                                self.peek()
3041                            )));
3042                        }
3043                        self.advance();
3044                        let table = self.expect_ident_like()?;
3045                        Ok(Statement::DropTrigger {
3046                            name,
3047                            table,
3048                            if_exists,
3049                        })
3050                    }
3051                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3052                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3053                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3054                        self.advance();
3055                        let if_exists = self.consume_if_exists();
3056                        let name = self.expect_ident_like()?;
3057                        if !matches!(self.peek(), Token::On) {
3058                            return Err(self.err(alloc::format!(
3059                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3060                                self.peek()
3061                            )));
3062                        }
3063                        self.advance();
3064                        let table = self.expect_ident_like()?;
3065                        // Optional CASCADE / RESTRICT — accepted, no effect.
3066                        self.consume_until_statement_boundary();
3067                        Ok(Statement::DropRule {
3068                            name,
3069                            table,
3070                            if_exists,
3071                        })
3072                    }
3073                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3074                    // v7.12.4 ignores any optional arg-list (signature-
3075                    // based overload disambiguation lands in v7.12.5+).
3076                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3077                        self.advance();
3078                        let if_exists = self.consume_if_exists();
3079                        let name = self.expect_ident_like()?;
3080                        // v7.39 (read01 round 62) — the argument list identifies
3081                        // WHICH overload to drop, so it is captured, not
3082                        // discarded. `DROP FUNCTION f` (no list) is legal when
3083                        // the name is unambiguous; the engine enforces that.
3084                        let args = if matches!(self.peek(), Token::LParen) {
3085                            Some(self.parse_function_signature_types()?)
3086                        } else {
3087                            None
3088                        };
3089                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3090                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3091                        // accepted since v7.14 and this one refused outright.
3092                        // pg_dump writes it, so refusing was a parse error in
3093                        // the middle of a restore. SPG drops the function
3094                        // either way — it tracks no dependents to cascade to —
3095                        // which is the same reading the other two give it.
3096                        self.consume_drop_behaviour();
3097                        Ok(Statement::DropFunction {
3098                            name,
3099                            args,
3100                            if_exists,
3101                        })
3102                    }
3103                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3104                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3105                    // emit DROP TABLE IF EXISTS at the head of every
3106                    // CREATE TABLE block so re-importing a dump
3107                    // overwrites prior state. SPG accepts and removes
3108                    // matching tables; CASCADE/RESTRICT trailers
3109                    // accepted silently.
3110                    Token::Table => self.parse_drop_table_after_keyword(),
3111                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3112                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3113                    // for partial-index renames and pgvector
3114                    // migrations. SPG removes the matching index;
3115                    // IF EXISTS makes the drop idempotent.
3116                    Token::Index => {
3117                        self.advance();
3118                        let if_exists = self.consume_if_exists();
3119                        let name = self.expect_ident_like()?;
3120                        if matches!(
3121                            self.peek(),
3122                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3123                                || s.eq_ignore_ascii_case("restrict")
3124                        ) {
3125                            self.advance();
3126                        }
3127                        Ok(Statement::DropIndex { name, if_exists })
3128                    }
3129                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3130                    // [CASCADE|RESTRICT]. SPG is single-database;
3131                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3132                    // name [, name…] [CASCADE | RESTRICT]. Real
3133                    // unregister (was silent no-op pre-v7.17).
3134                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3135                        self.advance();
3136                        let if_exists = self.consume_if_exists();
3137                        let mut names = vec![self.expect_ident_like()?];
3138                        while matches!(self.peek(), Token::Comma) {
3139                            self.advance();
3140                            names.push(self.expect_ident_like()?);
3141                        }
3142                        if matches!(
3143                            self.peek(),
3144                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3145                                || s.eq_ignore_ascii_case("restrict")
3146                        ) {
3147                            self.advance();
3148                        }
3149                        Ok(Statement::DropSchema { names, if_exists })
3150                    }
3151                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3152                    // name [, name…] [CASCADE|RESTRICT].
3153                    Token::Ident(s) | Token::QuotedIdent(s)
3154                        if s.eq_ignore_ascii_case("type") =>
3155                    {
3156                        self.advance();
3157                        let if_exists = self.consume_if_exists();
3158                        let mut names = vec![self.expect_ident_like()?];
3159                        while matches!(self.peek(), Token::Comma) {
3160                            self.advance();
3161                            names.push(self.expect_ident_like()?);
3162                        }
3163                        if matches!(
3164                            self.peek(),
3165                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3166                                || s.eq_ignore_ascii_case("restrict")
3167                        ) {
3168                            self.advance();
3169                        }
3170                        Ok(Statement::DropType { names, if_exists })
3171                    }
3172                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3173                    // name [, name…] [CASCADE|RESTRICT].
3174                    Token::Ident(s) | Token::QuotedIdent(s)
3175                        if s.eq_ignore_ascii_case("domain") =>
3176                    {
3177                        self.advance();
3178                        let if_exists = self.consume_if_exists();
3179                        let mut names = vec![self.expect_ident_like()?];
3180                        while matches!(self.peek(), Token::Comma) {
3181                            self.advance();
3182                            names.push(self.expect_ident_like()?);
3183                        }
3184                        if matches!(
3185                            self.peek(),
3186                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3187                                || s.eq_ignore_ascii_case("restrict")
3188                        ) {
3189                            self.advance();
3190                        }
3191                        Ok(Statement::DropDomain { names, if_exists })
3192                    }
3193                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3194                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3195                    Token::Ident(s) | Token::QuotedIdent(s)
3196                        if s.eq_ignore_ascii_case("materialized") =>
3197                    {
3198                        self.advance();
3199                        let nxt = self.peek().clone();
3200                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3201                        {
3202                            return Err(self.err(alloc::format!(
3203                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3204                            )));
3205                        }
3206                        self.advance();
3207                        let if_exists = self.consume_if_exists();
3208                        let mut names = vec![self.expect_ident_like()?];
3209                        while matches!(self.peek(), Token::Comma) {
3210                            self.advance();
3211                            names.push(self.expect_ident_like()?);
3212                        }
3213                        if matches!(
3214                            self.peek(),
3215                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3216                                || s.eq_ignore_ascii_case("restrict")
3217                        ) {
3218                            self.advance();
3219                        }
3220                        Ok(Statement::DropMaterializedView { names, if_exists })
3221                    }
3222                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3223                    // name [, name…] [CASCADE|RESTRICT].
3224                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3225                        self.advance();
3226                        let if_exists = self.consume_if_exists();
3227                        let mut names = vec![self.expect_ident_like()?];
3228                        while matches!(self.peek(), Token::Comma) {
3229                            self.advance();
3230                            names.push(self.expect_ident_like()?);
3231                        }
3232                        if matches!(
3233                            self.peek(),
3234                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3235                                || s.eq_ignore_ascii_case("restrict")
3236                        ) {
3237                            self.advance();
3238                        }
3239                        Ok(Statement::DropView { names, if_exists })
3240                    }
3241                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3242                    // [CASCADE|RESTRICT]. Real removal from catalog
3243                    // (was a silent no-op pre-v7.17).
3244                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3245                        self.advance();
3246                        let if_exists = self.consume_if_exists();
3247                        let mut names = vec![self.expect_ident_like()?];
3248                        while matches!(self.peek(), Token::Comma) {
3249                            self.advance();
3250                            names.push(self.expect_ident_like()?);
3251                        }
3252                        if matches!(
3253                            self.peek(),
3254                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3255                                || s.eq_ignore_ascii_case("restrict")
3256                        ) {
3257                            self.advance();
3258                        }
3259                        Ok(Statement::DropSequence { names, if_exists })
3260                    }
3261                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3262                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3263                        self.advance();
3264                        self.parse_drop_policy_after_keyword()
3265                    }
3266                    // v7.37.17 (17.6 siblings) — DROP <target> for
3267                    // targets SPG doesn't natively track. pg_dump
3268                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3269                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3270                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3271                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3272                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3273                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3274                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3275                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3276                    // etc. — accept + Empty-return so pg_dump tails
3277                    // load through. Materialized-view drop dispatches
3278                    // to the existing DropTable path when the token
3279                    // is Materialized-View-shaped (elsewhere in
3280                    // this parser).
3281                    Token::Ident(s) | Token::QuotedIdent(s)
3282                        if s.eq_ignore_ascii_case("text")
3283                            // The DROP dispatch matches on PEEK — `text` is
3284                            // not yet consumed, so SEARCH/CONFIGURATION sit
3285                            // at pos+1/pos+2 (the round-695 trap's mirror).
3286                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3287                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3288                    {
3289                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3290                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3291                        // stay in the noise arm below.
3292                        self.advance(); // TEXT
3293                        self.advance(); // SEARCH
3294                        self.advance(); // CONFIGURATION
3295                        let if_exists = self.consume_if_exists();
3296                        let names = self.take_comma_separated_names();
3297                        self.consume_until_statement_boundary();
3298                        if if_exists {
3299                            return Ok(Statement::Empty);
3300                        }
3301                        Ok(Statement::ValidateOnly {
3302                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3303                            names,
3304                        })
3305                    }
3306                    Token::Ident(s) | Token::QuotedIdent(s)
3307                        if matches!(
3308                            s.to_ascii_lowercase().as_str(),
3309                            "type"
3310                                | "domain"
3311                                | "operator"
3312                                | "cast"
3313                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3314                                // TEMPLATE (CONFIGURATION intercepted above).
3315                                | "text"
3316                                | "materialized"
3317                                | "large"
3318                                | "role"
3319                                | "access"
3320                                | "procedure"
3321                                | "routine"
3322                        ) =>
3323                    {
3324                        self.consume_until_statement_boundary();
3325                        Ok(Statement::Empty)
3326                    }
3327                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3328                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3329                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3330                    // foreign-data warning family (round 706) so a
3331                    // CREATE→DROP sequence in a dump stays consistent.
3332                    Token::Ident(s) | Token::QuotedIdent(s)
3333                        if s.eq_ignore_ascii_case("server")
3334                            || s.eq_ignore_ascii_case("foreign") =>
3335                    {
3336                        self.advance();
3337                        self.consume_until_statement_boundary();
3338                        Ok(Statement::ValidateOnly {
3339                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3340                            names: Vec::new(),
3341                        })
3342                    }
3343                    Token::Ident(s) | Token::QuotedIdent(s)
3344                        if s.eq_ignore_ascii_case("collation")
3345                            || s.eq_ignore_ascii_case("tablespace") =>
3346                    {
3347                        let kind = if s.eq_ignore_ascii_case("collation") {
3348                            crate::ast::ValidateOnlyKind::CollationName
3349                        } else {
3350                            crate::ast::ValidateOnlyKind::TablespaceName
3351                        };
3352                        self.advance();
3353                        let if_exists = self.consume_if_exists();
3354                        let names = self.take_comma_separated_names();
3355                        self.consume_until_statement_boundary();
3356                        if if_exists {
3357                            return Ok(Statement::Empty);
3358                        }
3359                        Ok(Statement::ValidateOnly { kind, names })
3360                    }
3361                    Token::Ident(s) | Token::QuotedIdent(s)
3362                        if s.eq_ignore_ascii_case("event") =>
3363                    {
3364                        self.advance();
3365                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3366                        {
3367                            self.advance();
3368                        }
3369                        let if_exists = self.consume_if_exists();
3370                        let names = self.take_comma_separated_names();
3371                        self.consume_until_statement_boundary();
3372                        if if_exists {
3373                            return Ok(Statement::Empty);
3374                        }
3375                        Ok(Statement::ValidateOnly {
3376                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3377                            names,
3378                        })
3379                    }
3380                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3381                    // leave the noise list; see the ValidateOnly kinds.
3382                    Token::Ident(s) | Token::QuotedIdent(s)
3383                        if s.eq_ignore_ascii_case("conversion")
3384                            || s.eq_ignore_ascii_case("language")
3385                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3386                            // FIRST — the first draft looked for it after.
3387                            || s.eq_ignore_ascii_case("procedural") =>
3388                    {
3389                        let kind = if s.eq_ignore_ascii_case("conversion") {
3390                            crate::ast::ValidateOnlyKind::ConversionName
3391                        } else {
3392                            crate::ast::ValidateOnlyKind::LanguageName
3393                        };
3394                        self.advance();
3395                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3396                        {
3397                            self.advance();
3398                        }
3399                        let if_exists = self.consume_if_exists();
3400                        let names = self.take_comma_separated_names();
3401                        self.consume_until_statement_boundary();
3402                        if if_exists {
3403                            return Ok(Statement::Empty);
3404                        }
3405                        Ok(Statement::ValidateOnly { kind, names })
3406                    }
3407                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3408                    // name(argtypes)[, …]`. Parsed for real so the engine
3409                    // can answer as PG does; see Statement::DropAggregate.
3410                    Token::Ident(s) | Token::QuotedIdent(s)
3411                        if s.eq_ignore_ascii_case("aggregate") =>
3412                    {
3413                        self.advance();
3414                        let if_exists = self.consume_if_exists();
3415                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3416                        loop {
3417                            let name = self.expect_ident_like()?;
3418                            if !matches!(self.peek(), Token::LParen) {
3419                                return Err(self.err(alloc::format!(
3420                                    "expected argument list after DROP AGGREGATE {name}"
3421                                )));
3422                            }
3423                            self.advance();
3424                            let mut args: Vec<String> = Vec::new();
3425                            let mut star = false;
3426                            loop {
3427                                match self.peek().clone() {
3428                                    Token::RParen => {
3429                                        self.advance();
3430                                        break;
3431                                    }
3432                                    Token::Star => {
3433                                        self.advance();
3434                                        star = true;
3435                                    }
3436                                    Token::Comma => {
3437                                        self.advance();
3438                                    }
3439                                    _ => {
3440                                        // A type name may be multi-token
3441                                        // (`double precision`); glue idents
3442                                        // until , or ).
3443                                        let mut t = self.expect_ident_like()?;
3444                                        while let Token::Ident(nx) = self.peek() {
3445                                            let nx = nx.clone();
3446                                            self.advance();
3447                                            t.push(' ');
3448                                            t.push_str(&nx);
3449                                        }
3450                                        args.push(t);
3451                                    }
3452                                }
3453                            }
3454                            items.push((name, if star { None } else { Some(args) }));
3455                            if matches!(self.peek(), Token::Comma) {
3456                                self.advance();
3457                            } else {
3458                                break;
3459                            }
3460                        }
3461                        self.consume_until_statement_boundary();
3462                        Ok(Statement::DropAggregate { if_exists, items })
3463                    }
3464                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3465                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3466                    // installed; `IF EXISTS` is the spelling that says do
3467                    // not, and it keeps the no-op.
3468                    Token::Ident(s) | Token::QuotedIdent(s)
3469                        if s.eq_ignore_ascii_case("extension") =>
3470                    {
3471                        self.advance();
3472                        let if_exists = self.consume_if_exists();
3473                        let names = self.take_comma_separated_names();
3474                        self.consume_until_statement_boundary();
3475                        if if_exists {
3476                            return Ok(Statement::Empty);
3477                        }
3478                        Ok(Statement::ValidateOnly {
3479                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3480                            names,
3481                        })
3482                    }
3483                    Token::Ident(s) | Token::QuotedIdent(s)
3484                        if s.eq_ignore_ascii_case("statistics") =>
3485                    {
3486                        self.parse_drop_statistics_after_drop()
3487                    }
3488                    other => Err(self.err(format!(
3489                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3490                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3491                    ))),
3492                }
3493            }
3494            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3495            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3496            // and accepted before the view name. SPG materialised
3497            // views re-evaluate on read (always-fresh semantics), so
3498            // the CONCURRENTLY-vs-serial distinction has no runtime
3499            // effect — the refresh body does not block readers either
3500            // way. Same accept-and-no-op pattern as DETACH PARTITION
3501            // CONCURRENTLY (16.5).
3502            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3503                self.advance();
3504                let nxt = self.peek().clone();
3505                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3506                {
3507                    return Err(self.err(alloc::format!(
3508                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3509                    )));
3510                }
3511                self.advance();
3512                let nxt2 = self.peek().clone();
3513                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3514                {
3515                    return Err(self.err(alloc::format!(
3516                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3517                    )));
3518                }
3519                self.advance();
3520                // Optional CONCURRENTLY noise word — consumed without
3521                // changing semantics.
3522                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3523                {
3524                    self.advance();
3525                }
3526                let name = self.expect_ident_like()?;
3527                let with_data = self.parse_optional_with_data(true)?;
3528                Ok(Statement::RefreshMaterializedView { name, with_data })
3529            }
3530            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3531                self.advance();
3532                self.parse_update_after_keyword()
3533            }
3534            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3535            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3536            // [CASCADE | RESTRICT]. Clears every row from each named
3537            // table. Parses at the top level; the engine dispatcher
3538            // walks Statement::Truncate.
3539            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3540                self.advance();
3541                // Optional TABLE noise word — PG accepts both the reserved
3542                // token and the bare identifier spelling.
3543                if matches!(self.peek(), Token::Table)
3544                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3545                {
3546                    self.advance();
3547                }
3548                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3549                // not absorbed. The lookahead keeps a table genuinely
3550                // called `only` working: the keyword is a keyword only
3551                // when a name follows it.
3552                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3553                    if s.eq_ignore_ascii_case("only"))
3554                    && matches!(
3555                        self.tokens.get(self.pos + 1),
3556                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3557                    );
3558                if only {
3559                    self.advance();
3560                }
3561                // Table names (comma-separated).
3562                let mut tables = Vec::new();
3563                loop {
3564                    tables.push(self.expect_ident_like()?);
3565                    if matches!(self.peek(), Token::Comma) {
3566                        self.advance();
3567                        continue;
3568                    }
3569                    break;
3570                }
3571                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3572                let mut restart_identity = false;
3573                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3574                {
3575                    self.advance();
3576                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3577                    {
3578                        self.advance();
3579                        restart_identity = true;
3580                    }
3581                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3582                {
3583                    self.advance();
3584                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3585                    {
3586                        self.advance();
3587                    }
3588                }
3589                // Optional CASCADE / RESTRICT.
3590                let mut cascade = false;
3591                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3592                {
3593                    self.advance();
3594                    cascade = true;
3595                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3596                {
3597                    self.advance();
3598                }
3599                Ok(Statement::Truncate {
3600                    tables,
3601                    restart_identity,
3602                    cascade,
3603                    only,
3604                })
3605            }
3606            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3607            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3608            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3609            // rows change so the index tree is always up-to-date;
3610            // REINDEX is a strict no-op. Accept the whole statement
3611            // shape to boundary for pg_dump round-trip compatibility.
3612            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3613                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3614                // index bloat to rebuild, so the work stays a no-op, but PG
3615                // validates what it was pointed at and this swallowed the
3616                // name at parse time — `REINDEX TABLE typo` reported
3617                // success. Measured on PG18: INDEX / TABLE name a relation,
3618                // SCHEMA a schema, SYSTEM nothing.
3619                self.advance();
3620                self.parse_reindex_tail()
3621            }
3622            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3623            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3624            // SPG has no MVCC bloat today (Phase D visibility map
3625            // queues with v7.38); the freezer collapses hot-tier
3626            // rows into cold segments automatically. VACUUM is a
3627            // no-op — pg_dump maintenance scripts and Discourse's
3628            // periodic-maintenance path both emit it.
3629            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3630            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3631            // actual bloat, so the pre-MVCC accept-and-ignore posture
3632            // became a silent no-op on a customer's manual reclaim.
3633            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3634            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3635            // ANALYZE is captured, the optional table name is captured.
3636            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3637                self.advance();
3638                // Parenthesised option list: absorb it.
3639                if matches!(self.peek(), Token::LParen) {
3640                    let mut depth = 0usize;
3641                    loop {
3642                        match self.advance() {
3643                            Token::LParen => depth += 1,
3644                            Token::RParen => {
3645                                depth -= 1;
3646                                if depth == 0 {
3647                                    break;
3648                                }
3649                            }
3650                            Token::Eof => break,
3651                            _ => {}
3652                        }
3653                    }
3654                }
3655                let mut analyze = false;
3656                let mut table: Option<String> = None;
3657                loop {
3658                    match self.peek() {
3659                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3660                        // an identifier, so the loop below broke out on it and
3661                        // dropped the table name: `VACUUM FULL nosuch` was
3662                        // accepted where `VACUUM nosuch` was refused.
3663                        Token::Full => {
3664                            self.advance();
3665                        }
3666                        Token::Ident(w) | Token::QuotedIdent(w) => {
3667                            let wl = w.to_ascii_lowercase();
3668                            match wl.as_str() {
3669                                "full" | "freeze" | "verbose" => {
3670                                    self.advance();
3671                                }
3672                                "analyze" | "analyse" => {
3673                                    analyze = true;
3674                                    self.advance();
3675                                }
3676                                _ => {
3677                                    table = Some(self.expect_ident_like()?);
3678                                    break;
3679                                }
3680                            }
3681                        }
3682                        _ => break,
3683                    }
3684                }
3685                // Optional trailing column list / anything else to the
3686                // statement boundary (PG accepts per-column ANALYZE).
3687                self.consume_until_statement_boundary();
3688                Ok(Statement::Vacuum { table, analyze })
3689            }
3690            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3691            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3692            // <index>. PG stores rows in physical order matching
3693            // an index; SPG's hot-tier is append-only + cold-tier
3694            // is segment-frozen, so clustering has no persistent
3695            // effect. Accept-and-no-op for pg_dump compat.
3696            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3697                // v7.39 (round 535) — same as REINDEX above: the relation is
3698                // carried so the engine can refuse one that does not exist.
3699                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3700                self.advance();
3701                self.parse_cluster_tail()
3702            }
3703            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3704            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3705            // optional string payload; UNLISTEN takes a channel or `*`.
3706            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3707                self.advance();
3708                let ch = match self.advance() {
3709                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3710                    other => {
3711                        return Err(self.err(format!(
3712                            "expected channel name after LISTEN, got {other:?}"
3713                        )));
3714                    }
3715                };
3716                Ok(Statement::Listen(ch))
3717            }
3718            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3719                self.advance();
3720                let channel = match self.advance() {
3721                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3722                    other => {
3723                        return Err(self.err(format!(
3724                            "expected channel name after NOTIFY, got {other:?}"
3725                        )));
3726                    }
3727                };
3728                let payload = if matches!(self.peek(), Token::Comma) {
3729                    self.advance();
3730                    match self.advance() {
3731                        Token::String(p) => Some(p),
3732                        other => {
3733                            return Err(self.err(format!(
3734                                "expected string payload after NOTIFY <channel>, got {other:?}"
3735                            )));
3736                        }
3737                    }
3738                } else {
3739                    None
3740                };
3741                Ok(Statement::Notify { channel, payload })
3742            }
3743            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3744                self.advance();
3745                match self.advance() {
3746                    Token::Star => Ok(Statement::Unlisten(None)),
3747                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3748                    other => Err(self.err(format!(
3749                        "expected channel name or * after UNLISTEN, got {other:?}"
3750                    ))),
3751                }
3752            }
3753            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3754            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3755            // process-wide write lock today; explicit LOCK has no
3756            // effect. Accept-and-no-op for pg_dump / migration
3757            // compat.
3758            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3759                self.advance();
3760                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3761                // engine holds a process-wide write lock), but the TABLE
3762                // NAME is now carried out so the engine can refuse one that
3763                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3764                // READ|WRITE` is a different statement with the same first
3765                // word; it keeps the old no-op, because a MySQL dump's
3766                // bracket names tables it is about to create.
3767                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3768                    if k.eq_ignore_ascii_case("tables"));
3769                if mysql_tables {
3770                    self.consume_until_statement_boundary();
3771                    return Ok(Statement::Empty);
3772                }
3773                if matches!(self.peek(), Token::Table) {
3774                    self.advance();
3775                }
3776                let names = self.take_comma_separated_names();
3777                self.consume_until_statement_boundary();
3778                Ok(Statement::ValidateOnly {
3779                    kind: crate::ast::ValidateOnlyKind::LockTable,
3780                    names,
3781                })
3782            }
3783            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3784            // durability marker + snapshot in PG. SPG has WAL
3785            // checkpointing on a byte / time schedule (v7.37.10
3786            // 60s / 4 MiB defaults). The bare statement parses to
3787            // `Statement::Empty` here (the no_std engine owns no
3788            // WAL / snapshot); v7.37 Epic Du wires the HOST
3789            // (embedded `Database::execute_buffered`, via
3790            // `sql_is_checkpoint`) to force an immediate synchronous
3791            // checkpoint through `Database::checkpoint` — a real
3792            // durability barrier, matching PG.
3793            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3794                self.advance();
3795                self.consume_until_statement_boundary();
3796                Ok(Statement::Empty)
3797            }
3798            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3799                self.advance();
3800                self.parse_delete_after_keyword()
3801            }
3802            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3803            // ALTER is not a reserved keyword in the lexer — handled
3804            // as a bare ident here.
3805            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3806                self.advance();
3807                self.parse_alter_after_keyword()
3808            }
3809            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3810            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3811            // additions needed.
3812            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3813                self.advance();
3814                self.parse_wait_after_keyword()
3815            }
3816            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3817            // Bare ANALYZE → analyse every user table; ANALYZE
3818            // <name> → re-stats one. The argument is an optional
3819            // ident (or quoted ident); anything else is a parse
3820            // error.
3821            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3822            // `WHERE` filter (carved out per V6_7_DESIGN.md
3823            // STABILITY). Lex order: identifier "compact" → "cold"
3824            // → "segments". Anything else after `COMPACT` is a
3825            // parse error.
3826            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3827                self.advance();
3828                let next = self.peek().clone();
3829                let cold = match next {
3830                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3831                    _ => {
3832                        return Err(
3833                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3834                        );
3835                    }
3836                };
3837                if !cold.eq_ignore_ascii_case("cold") {
3838                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3839                }
3840                self.advance();
3841                let next = self.peek().clone();
3842                let segments = match next {
3843                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3844                    _ => {
3845                        return Err(self.err(format!(
3846                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3847                            self.peek()
3848                        )));
3849                    }
3850                };
3851                if !segments.eq_ignore_ascii_case("segments") {
3852                    return Err(self.err(format!(
3853                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3854                    )));
3855                }
3856                self.advance();
3857                Ok(Statement::CompactColdSegments)
3858            }
3859            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3860            // Parsed as a case-insensitive identifier since MERGE
3861            // isn't a reserved lexer keyword (collides with the
3862            // mysqldump `ALGORITHM = MERGE` view clause if it
3863            // were); the inner parser drives the rest of the
3864            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3865            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3866                self.advance();
3867                self.parse_merge_after_keyword()
3868            }
3869            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3870                self.advance();
3871                let target = match self.peek() {
3872                    Token::Eof | Token::Semicolon => None,
3873                    Token::Ident(_) | Token::QuotedIdent(_) => {
3874                        Some(self.expect_ident_like()?)
3875                    }
3876                    other => {
3877                        return Err(self.err(format!(
3878                            "expected table name or end of statement after ANALYZE, got {other:?}"
3879                        )));
3880                    }
3881                };
3882                // v7.39 (round 776, F31 J7) — the per-column form
3883                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3884                // here while the VACUUM arm already consumed it; SPG
3885                // analyzes whole tables, so the list parses and is
3886                // accepted like the VACUUM path's.
3887                if target.is_some() && matches!(self.peek(), Token::LParen) {
3888                    self.advance();
3889                    loop {
3890                        let _ = self.expect_ident_like()?;
3891                        match self.peek() {
3892                            Token::Comma => {
3893                                self.advance();
3894                            }
3895                            Token::RParen => {
3896                                self.advance();
3897                                break;
3898                            }
3899                            other => {
3900                                return Err(self.err(format!(
3901                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3902                                )));
3903                            }
3904                        }
3905                    }
3906                }
3907                Ok(Statement::Analyze(target))
3908            }
3909            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3910            // `default_text_search_config` parameter is consumed
3911            // by the FTS function dispatcher; other parameter
3912            // names are recorded but treated as a no-op so PG
3913            // dump output loads.
3914            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3915                self.advance();
3916                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3917                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3918                // …` which the SessionVar path handles). `LOCAL` is the only
3919                // one that changes semantics — it scopes the change to the
3920                // current transaction — so capture it; SESSION / GLOBAL are
3921                // accepted and treated as the default session scope.
3922                let mut set_local = false;
3923                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3924                    let q = s.to_ascii_lowercase();
3925                    if q == "local" || q == "session" || q == "global" {
3926                        set_local = q == "local";
3927                        self.advance();
3928                    }
3929                }
3930                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3931                // { DEFAULT | <role> }`. pg_dump's ACL section switches
3932                // to the object owner with it. SPG maps it onto the
3933                // session-role machinery (recorded delta: PG moves
3934                // session_user too; SPG moves the effective role).
3935                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3936                    if s.eq_ignore_ascii_case("authorization"))
3937                {
3938                    self.advance(); // AUTHORIZATION
3939                    let role = match self.peek().clone() {
3940                        Token::Default => {
3941                            self.advance();
3942                            None
3943                        }
3944                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3945                            self.advance();
3946                            Some(s)
3947                        }
3948                        _ => None,
3949                    };
3950                    return Ok(Statement::SetRole(role));
3951                }
3952                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3953                // <collation>]` — change the connection client
3954                // charset. SPG stores UTF-8 always and orders
3955                // bytewise; accept as a no-op.
3956                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3957                {
3958                    self.advance();
3959                    // Charset ident-or-string.
3960                    if matches!(
3961                        self.peek(),
3962                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3963                    ) {
3964                        self.advance();
3965                    }
3966                    // Optional `COLLATE <name>`.
3967                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3968                    {
3969                        self.advance();
3970                        if matches!(
3971                            self.peek(),
3972                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3973                        ) {
3974                            self.advance();
3975                        }
3976                    }
3977                    return Ok(Statement::Empty);
3978                }
3979                // v7.37.17 (17.6 sibling) — PG `SET ROLE
3980                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3981                // uses this to switch to the object owner before
3982                // recreating tables. SPG has no role system so this
3983                // is a no-op.
3984                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3985                {
3986                    self.advance(); // ROLE
3987                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3988                    // reset to the login identity; a name / string sets the
3989                    // effective role that drives current_user + RLS.
3990                    let role = match self.peek().clone() {
3991                        Token::Default => {
3992                            self.advance();
3993                            None
3994                        }
3995                        Token::Ident(s) | Token::QuotedIdent(s)
3996                            if s.eq_ignore_ascii_case("none") =>
3997                        {
3998                            self.advance();
3999                            None
4000                        }
4001                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4002                            self.advance();
4003                            Some(s)
4004                        }
4005                        _ => None,
4006                    };
4007                    return Ok(Statement::SetRole(role));
4008                }
4009                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4010                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4011                // ISO SQL surface). pg_dump prepends this to fix
4012                // the isolation level for the restore session. SPG
4013                // defaults to READ COMMITTED and doesn't yet honor
4014                // session-set isolation across statements — accept
4015                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4016                // per-tx form is handled elsewhere.
4017                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4018                {
4019                    self.advance(); // CHARACTERISTICS
4020                    self.consume_until_statement_boundary();
4021                    return Ok(Statement::Empty);
4022                }
4023                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4024                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4025                // pg_dump emits this to control the deferrability of
4026                // FK / UNIQUE constraints across a bulk restore. SPG
4027                // has no deferrable-constraint machinery today; the
4028                // FK checker is strict-immediate. Accept-and-no-op
4029                // for pg_dump round-trip compatibility.
4030                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4031                {
4032                    self.advance(); // CONSTRAINTS
4033                    // v7.39 (round 288) — no longer a no-op: the trailing
4034                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4035                    // v7.39 (round 308, V29) — and the names are kept.
4036                    // They used to be skipped over on the way to the
4037                    // DEFERRED keyword, so a named form silently behaved
4038                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4039                    // every deferrable constraint in the transaction.
4040                    let mut names: alloc::vec::Vec<alloc::string::String> =
4041                        alloc::vec::Vec::new();
4042                    if matches!(self.peek(), Token::All) {
4043                        self.advance();
4044                    } else {
4045                        loop {
4046                            let mut n = self.expect_ident_like()?;
4047                            // A schema-qualified name (`public.fk_a`)
4048                            // identifies the same constraint; PG resolves
4049                            // it by the trailing segment.
4050                            while matches!(self.peek(), Token::Dot) {
4051                                self.advance();
4052                                n = self.expect_ident_like()?;
4053                            }
4054                            names.push(n);
4055                            if matches!(self.peek(), Token::Comma) {
4056                                self.advance();
4057                            } else {
4058                                break;
4059                            }
4060                        }
4061                    }
4062                    let deferred = match self.peek() {
4063                        Token::Ident(s) | Token::QuotedIdent(s)
4064                            if s.eq_ignore_ascii_case("deferred") =>
4065                        {
4066                            true
4067                        }
4068                        Token::Ident(s) | Token::QuotedIdent(s)
4069                            if s.eq_ignore_ascii_case("immediate") =>
4070                        {
4071                            false
4072                        }
4073                        other => {
4074                            return Err(self.err(alloc::format!(
4075                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4076                            )));
4077                        }
4078                    };
4079                    self.advance();
4080                    return Ok(Statement::SetConstraints { names, deferred });
4081                }
4082                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4083                // { DEFAULT | '<role>' | <ident> }` (mailrs
4084                // round-10 A.1). pg_dump preamble emits the
4085                // `DEFAULT` form to reset session authorization.
4086                //
4087                // v7.39 (round 697) — this said "SPG has no role system so
4088                // this is a strict no-op". SPG has had one since round 58;
4089                // the comment outlived it, and with it the reason a name
4090                // that is not a role was accepted here. It still switches
4091                // no authorization — what it does now is refuse a role
4092                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4093                // AUTHORIZATION` (handled by the RESET parser
4094                // elsewhere). Reference:
4095                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4096                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4097                {
4098                    self.advance(); // AUTHORIZATION
4099                    match self.peek().clone() {
4100                        Token::Default => {
4101                            self.advance();
4102                        }
4103                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4104                            self.advance();
4105                            return Ok(Statement::ValidateOnly {
4106                                kind: crate::ast::ValidateOnlyKind::RoleName,
4107                                names: alloc::vec![r],
4108                            });
4109                        }
4110                        other => {
4111                            return Err(self.err(alloc::format!(
4112                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4113                            )));
4114                        }
4115                    }
4116                    return Ok(Statement::Empty);
4117                }
4118                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4119                // ISOLATION LEVEL { READ COMMITTED | READ
4120                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4121                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4122                // PG-standard surface. v7.37.8 accepts the syntax
4123                // and tracks the selected level on
4124                // `Engine::current_isolation_level()`; the actual
4125                // MVCC / SSI semantics implementation lands in
4126                // the 轴 4 isolation framework (separate train).
4127                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4128                // internally; SPG behaves the same (effectively
4129                // READ COMMITTED at every level today).
4130                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4131                {
4132                    self.advance(); // TRANSACTION
4133                    let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
4134                    return Ok(Statement::SetTransaction { isolation: level });
4135                }
4136                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4137                // alias — same accept-as-no-op as SET NAMES.
4138                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4139                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4140                {
4141                    self.advance(); // CHARACTER
4142                    self.advance(); // SET
4143                    if matches!(
4144                        self.peek(),
4145                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4146                    ) {
4147                        self.advance();
4148                    }
4149                    return Ok(Statement::Empty);
4150                }
4151                // v7.39 (GUC) — PG spells the timezone GUC as two
4152                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4153                // where <value> is a string/ident or the LOCAL /
4154                // DEFAULT keyword (both mean "back to the default").
4155                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4156                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4157                {
4158                    self.advance(); // TIME
4159                    self.advance(); // ZONE
4160                    let value = match self.peek().clone() {
4161                        Token::Ident(s)
4162                            if s.eq_ignore_ascii_case("local")
4163                                || s.eq_ignore_ascii_case("default") =>
4164                        {
4165                            self.advance();
4166                            crate::ast::SetValue::Default
4167                        }
4168                        Token::Default => {
4169                            self.advance();
4170                            crate::ast::SetValue::Default
4171                        }
4172                        _ => self.parse_set_value()?,
4173                    };
4174                    return Ok(Statement::SetParameter {
4175                        name: "timezone".into(),
4176                        value,
4177                        local: set_local,
4178                    });
4179                }
4180                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4181                // MySQL USER-variable assignment: its own per-session
4182                // namespace, an arbitrary expression on the right, and `:=`
4183                // as a second spelling of `=`. It used to fall into the
4184                // session-PARAMETER list below, whose values are literals and
4185                // whose store nothing reads back under a `@` name — so the
4186                // assignment reported success and vanished.
4187                //
4188                // A `@@`-prefixed LHS is a real engine setting and keeps the
4189                // old path.
4190                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4191                    return self.parse_set_user_vars();
4192                }
4193                // v7.14.0 — multi-assignment form
4194                // `SET a = 1, b = 2, …`. Single-assignment is the
4195                // 1-element case. Each LHS may be a regular ident
4196                // or a SessionVar (`@VAR` / `@@VAR`).
4197                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4198                loop {
4199                    let lhs = match self.peek().clone() {
4200                        Token::SessionVar(s) => {
4201                            self.advance();
4202                            s
4203                        }
4204                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4205                        other => {
4206                            return Err(self.err(format!(
4207                                "expected parameter name after SET, got {other:?}"
4208                            )));
4209                        }
4210                    };
4211                    // Accept either `=` or the bare `TO` keyword.
4212                    match self.peek() {
4213                        Token::Eq => {
4214                            self.advance();
4215                        }
4216                        Token::To => {
4217                            self.advance();
4218                        }
4219                        other => {
4220                            return Err(self.err(format!(
4221                                "expected `=` or TO after SET {lhs}, got {other:?}"
4222                            )));
4223                        }
4224                    }
4225                    let mut value = self.parse_set_value()?;
4226                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4227                    // `, name TO` continues a MySQL-style multi-assign,
4228                    // anything else is a PG list VALUE
4229                    // (`SET search_path = myschema, public`) folded into
4230                    // one comma-joined string.
4231                    while matches!(self.peek(), Token::Comma) {
4232                        let is_assign = matches!(
4233                            self.tokens.get(self.pos + 1),
4234                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4235                        ) && matches!(
4236                            self.tokens.get(self.pos + 2),
4237                            Some(Token::Eq | Token::To)
4238                        );
4239                        if is_assign {
4240                            break;
4241                        }
4242                        self.advance(); // comma
4243                        let next = self.parse_set_value()?;
4244                        let joined = alloc::format!(
4245                            "{}, {}",
4246                            set_value_text(&value),
4247                            set_value_text(&next)
4248                        );
4249                        value = crate::ast::SetValue::String(joined);
4250                    }
4251                    pairs.push((lhs, value));
4252                    if matches!(self.peek(), Token::Comma) {
4253                        self.advance();
4254                        continue;
4255                    }
4256                    break;
4257                }
4258                if pairs.len() == 1 {
4259                    let (name, value) = pairs.into_iter().next().unwrap();
4260                    Ok(Statement::SetParameter {
4261                        name,
4262                        value,
4263                        local: set_local,
4264                    })
4265                } else {
4266                    Ok(Statement::SetParameterList(pairs))
4267                }
4268            }
4269            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4270            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4271                self.advance();
4272                match self.peek().clone() {
4273                    Token::All => {
4274                        self.advance();
4275                        Ok(Statement::ResetParameter(None))
4276                    }
4277                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4278                        self.advance();
4279                        Ok(Statement::ResetParameter(None))
4280                    }
4281                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4282                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4283                        self.advance();
4284                        Ok(Statement::SetRole(None))
4285                    }
4286                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4287                    // (pg_dump's return from the owner switch).
4288                    Token::Ident(s) | Token::QuotedIdent(s)
4289                        if s.eq_ignore_ascii_case("session")
4290                            && matches!(
4291                                self.tokens.get(self.pos + 1),
4292                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4293                                    if a.eq_ignore_ascii_case("authorization")
4294                            ) =>
4295                    {
4296                        self.advance(); // SESSION
4297                        self.advance(); // AUTHORIZATION
4298                        Ok(Statement::SetRole(None))
4299                    }
4300                    _ => {
4301                        let name = self.parse_set_param_name()?;
4302                        Ok(Statement::ResetParameter(Some(name)))
4303                    }
4304                }
4305            }
4306            // v7.39 (round 218) — server-side cursors.
4307            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4308            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4309            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4310            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4311                self.advance();
4312                match self.peek().clone() {
4313                    Token::All => {
4314                        self.advance();
4315                        Ok(Statement::CloseCursor { name: None })
4316                    }
4317                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4318                        self.advance();
4319                        Ok(Statement::CloseCursor { name: None })
4320                    }
4321                    Token::Ident(n) | Token::QuotedIdent(n) => {
4322                        self.advance();
4323                        Ok(Statement::CloseCursor { name: Some(n) })
4324                    }
4325                    other => Err(self.err(format!(
4326                        "expected cursor name or ALL after CLOSE, got {other:?}"
4327                    ))),
4328                }
4329            }
4330            other => Err(self.err(format!(
4331                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4332                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4333            ))),
4334        }
4335    }
4336
4337    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4338    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4339    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4340    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4341    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4342        self.advance(); // DECLARE
4343        let name = match self.advance() {
4344            Token::Ident(n) | Token::QuotedIdent(n) => n,
4345            other => {
4346                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4347            }
4348        };
4349        let mut scroll: Option<bool> = None;
4350        loop {
4351            match self.peek() {
4352                Token::Ident(s)
4353                    if s.eq_ignore_ascii_case("binary")
4354                        || s.eq_ignore_ascii_case("insensitive")
4355                        || s.eq_ignore_ascii_case("asensitive") =>
4356                {
4357                    self.advance();
4358                }
4359                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4360                    self.advance();
4361                    scroll = Some(true);
4362                }
4363                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4364                {
4365                    self.advance(); // NO
4366                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4367                        return Err(self.err(format!(
4368                            "expected SCROLL after NO in DECLARE, got {:?}",
4369                            self.peek()
4370                        )));
4371                    }
4372                    self.advance();
4373                    scroll = Some(false);
4374                }
4375                _ => break,
4376            }
4377        }
4378        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4379            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4380        }
4381        self.advance();
4382        let mut hold = false;
4383        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4384            self.advance();
4385            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4386                return Err(self.err(format!(
4387                    "expected HOLD after WITH in DECLARE, got {:?}",
4388                    self.peek()
4389                )));
4390            }
4391            self.advance();
4392            hold = true;
4393        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4394            self.advance();
4395            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4396                return Err(self.err(format!(
4397                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4398                    self.peek()
4399                )));
4400            }
4401            self.advance();
4402        }
4403        if !matches!(self.peek(), Token::For) {
4404            return Err(self.err(format!(
4405                "expected FOR before the cursor query, got {:?}",
4406                self.peek()
4407            )));
4408        }
4409        self.advance();
4410        let query = self.parse_one_statement()?;
4411        Ok(Statement::DeclareCursor {
4412            name,
4413            scroll,
4414            hold,
4415            query: alloc::boxed::Box::new(query),
4416        })
4417    }
4418
4419    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4420    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4421    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4422    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4423        use crate::ast::CursorDirection as D;
4424        self.advance(); // FETCH / MOVE
4425        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4426            let neg = if matches!(this.peek(), Token::Minus) {
4427                this.advance();
4428                true
4429            } else {
4430                false
4431            };
4432            match this.advance() {
4433                Token::Integer(v) => Ok(if neg { -v } else { v }),
4434                other => Err(this.err(format!("expected count, got {other:?}"))),
4435            }
4436        };
4437        let direction = match self.peek().clone() {
4438            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4439                self.advance();
4440                D::Next
4441            }
4442            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4443                self.advance();
4444                D::Prior
4445            }
4446            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4447                self.advance();
4448                D::First
4449            }
4450            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4451                self.advance();
4452                D::Last
4453            }
4454            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4455                self.advance();
4456                D::Absolute(signed_count(self)?)
4457            }
4458            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4459                self.advance();
4460                D::Relative(signed_count(self)?)
4461            }
4462            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4463                self.advance();
4464                match self.peek().clone() {
4465                    Token::All => {
4466                        self.advance();
4467                        D::All
4468                    }
4469                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4470                        self.advance();
4471                        D::All
4472                    }
4473                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4474                    _ => D::Next, // bare FORWARD = FORWARD 1
4475                }
4476            }
4477            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4478                self.advance();
4479                match self.peek().clone() {
4480                    Token::All => {
4481                        self.advance();
4482                        D::BackwardAll
4483                    }
4484                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4485                        self.advance();
4486                        D::BackwardAll
4487                    }
4488                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4489                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4490                }
4491            }
4492            Token::All => {
4493                self.advance();
4494                D::All
4495            }
4496            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4497                self.advance();
4498                D::All
4499            }
4500            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4501            // Bare `FETCH <name>` — direction defaults to NEXT.
4502            _ => D::Next,
4503        };
4504        // Optional FROM / IN.
4505        if matches!(self.peek(), Token::From)
4506            || matches!(self.peek(), Token::In)
4507            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4508        {
4509            self.advance();
4510        }
4511        let name = match self.advance() {
4512            Token::Ident(n) | Token::QuotedIdent(n) => n,
4513            other => {
4514                return Err(self.err(format!("expected cursor name, got {other:?}")));
4515            }
4516        };
4517        Ok(if is_move {
4518            Statement::MoveCursor { name, direction }
4519        } else {
4520            Statement::FetchCursor { name, direction }
4521        })
4522    }
4523
4524    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4525    /// [(kind, …)] ON <col>, … FROM <table>`.
4526    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4527        self.advance(); // STATISTICS
4528        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4529        let mut if_not_exists = false;
4530        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4531            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4532        {
4533            self.advance();
4534            self.advance();
4535            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4536                self.advance();
4537                if_not_exists = true;
4538            }
4539        }
4540        let name = self.expect_ident_like()?;
4541        let mut kinds = Vec::new();
4542        if matches!(self.peek(), Token::LParen) {
4543            self.advance();
4544            loop {
4545                let k = self.expect_ident_like()?;
4546                // PG stores the single letters; accept the spelled-out
4547                // names the SQL uses and record what PG records.
4548                kinds.push(match k.to_ascii_lowercase().as_str() {
4549                    "ndistinct" => String::from("d"),
4550                    "dependencies" => String::from("f"),
4551                    "mcv" => String::from("m"),
4552                    other => {
4553                        return Err(
4554                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4555                        );
4556                    }
4557                });
4558                match self.advance() {
4559                    Token::Comma => {}
4560                    Token::RParen => break,
4561                    other => {
4562                        return Err(self.err(alloc::format!(
4563                            "expected ',' or ')' in statistics kind list, got {other:?}"
4564                        )));
4565                    }
4566                }
4567            }
4568        }
4569        if !matches!(self.peek(), Token::On) {
4570            return Err(self.err(alloc::format!(
4571                "expected ON in CREATE STATISTICS, got {:?}",
4572                self.peek()
4573            )));
4574        }
4575        self.advance();
4576        let mut columns = Vec::new();
4577        loop {
4578            columns.push(self.expect_ident_like()?);
4579            if matches!(self.peek(), Token::Comma) {
4580                self.advance();
4581            } else {
4582                break;
4583            }
4584        }
4585        if !matches!(self.peek(), Token::From) {
4586            return Err(self.err(alloc::format!(
4587                "expected FROM in CREATE STATISTICS, got {:?}",
4588                self.peek()
4589            )));
4590        }
4591        self.advance();
4592        let table = self.expect_ident_like()?;
4593        Ok(Statement::CreateStatistics {
4594            name,
4595            if_not_exists,
4596            kinds,
4597            columns,
4598            table,
4599        })
4600    }
4601
4602    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4603    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4604    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4605    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4606    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4607    /// forward call.
4608    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4609        self.advance(); // TABLE
4610        let if_exists = self.consume_if_exists();
4611        let mut names: Vec<String> = Vec::new();
4612        loop {
4613            names.push(self.expect_ident_like()?);
4614            if matches!(self.peek(), Token::Comma) {
4615                self.advance();
4616                continue;
4617            }
4618            break;
4619        }
4620        if matches!(
4621            self.peek(),
4622            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4623                || s.eq_ignore_ascii_case("restrict")
4624        ) {
4625            self.advance();
4626        }
4627        Ok(Statement::DropTable { names, if_exists })
4628    }
4629
4630    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4631        self.advance(); // STATISTICS
4632        let mut if_exists = false;
4633        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4634            && matches!(self.tokens.get(self.pos + 1),
4635                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4636        {
4637            self.advance();
4638            self.advance();
4639            if_exists = true;
4640        }
4641        let name = self.expect_ident_like()?;
4642        Ok(Statement::DropStatistics { name, if_exists })
4643    }
4644
4645    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4646        debug_assert!(matches!(self.peek(), Token::Create));
4647        self.advance();
4648        match self.peek() {
4649            Token::Table => self.parse_create_table_stmt_after_create(),
4650            Token::Index => self.parse_create_index_stmt_after_create(false),
4651            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4652            // object now. It used to be consumed by the CREATE-noise
4653            // arm, so a pg_dump that declares extended statistics
4654            // restored silently without them and reflection showed
4655            // nothing.
4656            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4657                self.parse_create_statistics_after_create()
4658            }
4659            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4660            // The `UNIQUE` modifier turns a partial index into a
4661            // partial-uniqueness invariant (only rows matching the
4662            // WHERE predicate are checked for duplicates). mailrs
4663            // K1 (3 hits: email_templates default, calendar_events
4664            // master, calendar_events instance).
4665            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4666                self.advance();
4667                if !matches!(self.peek(), Token::Index) {
4668                    return Err(self.err(alloc::format!(
4669                        "expected INDEX after CREATE UNIQUE, got {:?}",
4670                        self.peek()
4671                    )));
4672                }
4673                self.parse_create_index_stmt_after_create(true)
4674            }
4675            Token::Publication => {
4676                self.advance();
4677                self.parse_create_publication_after_keyword()
4678            }
4679            Token::Subscription => {
4680                self.advance();
4681                self.parse_create_subscription_after_keyword()
4682            }
4683            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4684            // USER isn't a reserved keyword — we look for the bare
4685            // identifier so the lexer doesn't have to grow a token.
4686            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4687                self.advance();
4688                self.parse_create_user_after_keyword(true)
4689            }
4690            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4691            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4692            // the default of the LOGIN attribute.
4693            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4694                self.advance();
4695                self.parse_create_user_after_keyword(false)
4696            }
4697            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4698            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4699                self.advance();
4700                self.parse_create_policy_after_keyword()
4701            }
4702            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4703            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4704            // no-op. mailrs follow-up F3.
4705            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4706                self.advance();
4707                self.parse_create_extension_after_keyword()
4708            }
4709            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4710            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4711            // optional; absorb it here and forward to the
4712            // per-kind parsers with the flag. OR is a reserved
4713            // keyword token.
4714            Token::Or => {
4715                self.advance();
4716                let next = self.peek();
4717                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4718                    return Err(self.err(alloc::format!(
4719                        "expected REPLACE after CREATE OR, got {next:?}"
4720                    )));
4721                };
4722                if !s2.eq_ignore_ascii_case("replace") {
4723                    return Err(self.err(alloc::format!(
4724                        "expected REPLACE after CREATE OR, got {s2:?}"
4725                    )));
4726                }
4727                self.advance();
4728                self.parse_create_function_or_trigger_after_or_replace(true)
4729            }
4730            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4731                self.advance();
4732                self.parse_create_function_after_keyword(false)
4733            }
4734            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4735                self.advance();
4736                self.parse_create_trigger_after_keyword(false)
4737            }
4738            // v7.39 (round 139) — CREATE RULE …
4739            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4740                self.advance();
4741                self.parse_create_rule_after_keyword(false)
4742            }
4743            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4744            // trigger is a row-level AFTER trigger that additionally carries
4745            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4746            // path already tolerates and skips those clauses, so consuming the
4747            // CONSTRAINT keyword and reusing it makes the statement parse and the
4748            // trigger fire. (The deferral timing itself is not yet honoured —
4749            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4750            // for every non-deferred use.)
4751            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4752                self.advance();
4753                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4754                    if t.eq_ignore_ascii_case("trigger"))
4755                {
4756                    return Err(self.err(alloc::format!(
4757                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4758                        self.peek()
4759                    )));
4760                }
4761                self.advance();
4762                self.parse_create_trigger_after_keyword(false)
4763            }
4764            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4765            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4766                self.advance();
4767                self.parse_create_sequence_after_keyword(false)
4768            }
4769            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4770            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4771                self.advance();
4772                self.parse_create_view_after_keyword(false, false, false)
4773            }
4774            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4775            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4776            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4777            // appear (in any order) between `CREATE` and `VIEW` in
4778            // every mysqldump-emitted view. Pre-2.6 the parser
4779            // rejected the prefix and the customer's whole view
4780            // backup failed on the first view. The hints are pure
4781            // planner / permission metadata; SPG's view-rewrite
4782            // path is semantically equivalent for all three
4783            // algorithms in v7.17 (TEMPTABLE differs only in
4784            // perf for huge views — out of v7.17 scope), and
4785            // DEFINER / SQL SECURITY are pure single-user
4786            // permissioning that SPG ignores by design.
4787            Token::Ident(s) | Token::QuotedIdent(s)
4788                if s.eq_ignore_ascii_case("algorithm")
4789                    || s.eq_ignore_ascii_case("definer")
4790                    || s.eq_ignore_ascii_case("sql") =>
4791            {
4792                self.consume_mysql_view_prefix()?;
4793                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4794                // (in any order, in any combination), the next
4795                // keyword must be VIEW. mysqldump never emits these
4796                // prefixes on non-view statements.
4797                let next = self.peek().clone();
4798                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4799                    if s2.eq_ignore_ascii_case("view"))
4800                {
4801                    self.advance();
4802                    self.parse_create_view_after_keyword(false, false, false)
4803                } else {
4804                    Err(self.err(alloc::format!(
4805                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4806                    )))
4807                }
4808            }
4809            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4810            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4811                self.advance();
4812                self.parse_create_type_after_keyword()
4813            }
4814            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4815            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4816            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4817                self.advance();
4818                self.parse_create_domain_after_keyword()
4819            }
4820            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4821            // name [AUTHORIZATION user]. Real catalog registry
4822            // (was silent-no-op'd pre-v7.17).
4823            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4824                self.advance();
4825                let if_not_exists = self.parse_if_not_exists();
4826                let name = self.expect_ident_like()?;
4827                // Optional `AUTHORIZATION <user>` trailer — accepted,
4828                // ignored (single-user catalog).
4829                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4830                    if s.eq_ignore_ascii_case("authorization"))
4831                {
4832                    self.advance();
4833                    let _ = self.expect_ident_like()?;
4834                }
4835                Ok(Statement::CreateSchema { name, if_not_exists })
4836            }
4837            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4838            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4839                self.advance();
4840                let next = self.peek().clone();
4841                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4842                {
4843                    self.advance();
4844                    self.parse_create_materialized_view_after_keyword()
4845                } else {
4846                    Err(self.err(alloc::format!(
4847                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4848                    )))
4849                }
4850            }
4851            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4852            // no-op below), an UNLOGGED table is a real, fully-usable table in
4853            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4854            // durability optimisation is a follow-up), so a dump / app that
4855            // declares UNLOGGED tables works instead of failing to parse.
4856            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4857                self.advance(); // UNLOGGED
4858                if matches!(self.peek(), Token::Table) {
4859                    self.parse_create_table_stmt_after_create()
4860                } else {
4861                    Err(self.err(format!(
4862                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4863                        self.peek()
4864                    )))
4865                }
4866            }
4867            Token::Ident(s) | Token::QuotedIdent(s)
4868                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4869            {
4870                self.advance();
4871                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4872                let next = self.peek().clone();
4873                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4874                {
4875                    self.advance();
4876                    self.parse_create_sequence_after_keyword(true)
4877                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4878                {
4879                    self.advance();
4880                    self.parse_create_view_after_keyword(false, false, true)
4881                } else {
4882                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4883                    // consumed and answered OK while creating nothing, so
4884                    // every statement that touched the table afterwards failed
4885                    // with "table not found" — the DDL itself lied. It is a
4886                    // real CREATE TABLE now, marked temporary so the executor
4887                    // puts it in the session's own namespace. An optional
4888                    // TABLE keyword may or may not be present (`CREATE TEMP t`
4889                    // is not legal, but the keyword is consumed by the
4890                    // CREATE TABLE parser itself).
4891                    let stmt = self.parse_create_table_stmt_after_create()?;
4892                    match stmt {
4893                        Statement::CreateTable(mut c) => {
4894                            c.temporary = true;
4895                            Ok(Statement::CreateTable(c))
4896                        }
4897                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4898                        // CTAS node, which needs the same session namespace.
4899                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4900                            m.temporary = true;
4901                            Ok(Statement::CreateMaterializedView(m))
4902                        }
4903                        other => Ok(other),
4904                    }
4905                }
4906            }
4907            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4908            // BEGIN <body> END`. The body may reference `@var`
4909            // session variables, SET statements, internal `;`
4910            // terminators, etc. SPG has no procedure runtime, so
4911            // consume the whole `CREATE PROCEDURE … END` block as
4912            // a no-op so mysqldump scripts that include stored
4913            // routines load through. The matching-END consumer
4914            // tracks BEGIN/END nesting depth to handle nested
4915            // BEGIN blocks correctly.
4916            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4917                self.consume_mysql_routine_body();
4918                Ok(Statement::Empty)
4919            }
4920            // v7.14.0 — pg_dump / mysqldump emit
4921            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4922            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4923            // SPG is single-schema / single-database; these have
4924            // no behavioural effect, so consume + return Empty.
4925            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4926            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4927            // moved up to real parser branches. DATABASE / ROLE /
4928            // POLICY / OPERATOR stay no-op forever
4929            // (single-database, hardcoded roles).
4930            Token::Ident(s) | Token::QuotedIdent(s)
4931                if matches!(
4932                    s.to_ascii_lowercase().as_str(),
4933                    "database"
4934                        | "role"
4935                        | "operator"
4936                        | "cast"
4937                        | "aggregate"
4938                        | "language"
4939                        | "collation"
4940                        | "conversion"
4941                        // v7.17.0 Phase 8 (audit N6) — rarely-
4942                        // emitted pg_dump shapes that should
4943                        // load through without a parser error.
4944                        // SPG has no planner statistics catalog,
4945                        // no event-trigger hooks, no foreign-
4946                        // data-wrapper infrastructure; consume
4947                        // + return Empty.
4948                        | "statistics"
4949                        | "event"
4950                        // v7.37.17 (17.6 siblings) — additional CREATE
4951                        // targets pg_dump / operator install scripts
4952                        // may emit that SPG has no matching machinery
4953                        // for. Consume + Empty-return.
4954                        | "text"
4955                        | "tablespace"
4956                        | "access"
4957                        | "large"
4958                ) =>
4959            {
4960                // DATABASE is the one member of this list PG refuses
4961                // inside a transaction block; the rest (ROLE, CAST,
4962                // TABLESPACE, …) it runs there quite happily, so only
4963                // this one is named. Still a no-op otherwise — SPG is
4964                // single-database.
4965                let is_database = s.eq_ignore_ascii_case("database");
4966                let collation = if is_database {
4967                    self.scan_database_collation_until_boundary()
4968                } else {
4969                    self.consume_until_statement_boundary();
4970                    None
4971                };
4972                if is_database {
4973                    return Ok(Statement::NoOpPreventedInTransaction {
4974                        what: String::from("CREATE DATABASE"),
4975                        collation,
4976                    });
4977                }
4978                Ok(Statement::Empty)
4979            }
4980            // v7.39 (round 706) — the foreign-data family leaves the silent
4981            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4982            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4983            // FDW machinery), but the ENGINE now warns, so a restore log
4984            // says what will not function instead of reporting success.
4985            Token::Ident(s) | Token::QuotedIdent(s)
4986                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4987            {
4988                self.consume_until_statement_boundary();
4989                Ok(Statement::ValidateOnly {
4990                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4991                    names: Vec::new(),
4992                })
4993            }
4994            other => Err(self.err(format!(
4995                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4996            ))),
4997        }
4998    }
4999
5000    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5001    /// keyword decides whether we parse a function or trigger
5002    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5003    /// PROCEDURE) — those land in later releases.
5004    fn parse_create_function_or_trigger_after_or_replace(
5005        &mut self,
5006        or_replace: bool,
5007    ) -> Result<Statement, ParseError> {
5008        let tok = self.peek();
5009        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5010            return Err(self.err(alloc::format!(
5011                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5012            )));
5013        };
5014        if s.eq_ignore_ascii_case("function") {
5015            self.advance();
5016            self.parse_create_function_after_keyword(or_replace)
5017        } else if s.eq_ignore_ascii_case("trigger") {
5018            self.advance();
5019            self.parse_create_trigger_after_keyword(or_replace)
5020        } else if s.eq_ignore_ascii_case("rule") {
5021            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5022            self.advance();
5023            self.parse_create_rule_after_keyword(or_replace)
5024        } else if s.eq_ignore_ascii_case("view") {
5025            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5026            self.advance();
5027            self.parse_create_view_after_keyword(or_replace, false, false)
5028        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5029            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5030            self.advance();
5031            let nxt = self.peek().clone();
5032            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5033            {
5034                self.advance();
5035                self.parse_create_view_after_keyword(or_replace, false, true)
5036            } else {
5037                Err(self.err(alloc::format!(
5038                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5039                )))
5040            }
5041        } else {
5042            Err(self.err(alloc::format!(
5043                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5044            )))
5045        }
5046    }
5047
5048    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5049    /// SPG doesn't have a registry; pgvector / similar are
5050    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5051    /// the syntax lets dual-target schemas keep the line.
5052    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5053        // Optional `IF NOT EXISTS`.
5054        self.consume_if_not_exists();
5055        let name = self.expect_ident_like()?;
5056        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5057        // CASCADE / FROM '<v>' clauses; we don't model them.
5058        loop {
5059            match self.peek() {
5060                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5061                    self.advance();
5062                    continue;
5063                }
5064                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5065                    self.advance();
5066                    let _ = self.expect_ident_like()?;
5067                    continue;
5068                }
5069                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5070                    self.advance();
5071                    // String or ident literal.
5072                    let _ = self.advance();
5073                    continue;
5074                }
5075                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5076                    self.advance();
5077                    let _ = self.advance();
5078                    continue;
5079                }
5080                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5081                    self.advance();
5082                    continue;
5083                }
5084                _ => break,
5085            }
5086        }
5087        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5088        // nosuch` reported success and `pg_extension` then did not list it,
5089        // which is the accept-and-do-nothing shape F31 exists to find.
5090        Ok(Statement::ValidateOnly {
5091            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5092            names: alloc::vec![name],
5093        })
5094    }
5095
5096    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5097    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5098    /// already been consumed by the caller. Grammar accepted:
5099    ///
5100    ///   name `(` arg-list `)`
5101    ///   `RETURNS` return-type
5102    ///   [ `LANGUAGE` ident ]
5103    ///   `AS` $$ body $$
5104    ///   [ `LANGUAGE` ident ]
5105    ///
5106    /// Either `LANGUAGE` position is allowed; PG accepts both.
5107    fn parse_create_function_after_keyword(
5108        &mut self,
5109        or_replace: bool,
5110    ) -> Result<Statement, ParseError> {
5111        let name = self.expect_ident_like()?;
5112        // Argument list. v7.12.4 commonly sees the empty `()`
5113        // (trigger functions); typed args parse and round-trip
5114        // but the executor only invokes nullary functions.
5115        if !matches!(self.peek(), Token::LParen) {
5116            return Err(self.err(alloc::format!(
5117                "expected '(' after function name {name:?}, got {:?}",
5118                self.peek()
5119            )));
5120        }
5121        self.advance();
5122        let args = self.parse_function_arg_list()?;
5123        // RETURNS clause.
5124        let tok = self.peek();
5125        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5126            return Err(self.err(alloc::format!(
5127                "expected RETURNS after function arg list, got {tok:?}"
5128            )));
5129        };
5130        if !s.eq_ignore_ascii_case("returns") {
5131            return Err(self.err(alloc::format!(
5132                "expected RETURNS after function arg list, got {s:?}"
5133            )));
5134        }
5135        self.advance();
5136        let returns = self.parse_function_return()?;
5137        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5138        // re-check after the body too).
5139        let mut language: Option<String> = self.parse_optional_language()?;
5140        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5141        // either side of the body and in any order, interleaved with
5142        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5143        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5144        // PG's own pg_dump output did not restore.
5145        let mut attrs = FunctionAttrs::default();
5146        loop {
5147            let before = self.pos;
5148            self.parse_function_attrs_into(&mut attrs)?;
5149            if language.is_none() {
5150                language = self.parse_optional_language()?;
5151            }
5152            if self.pos == before {
5153                break;
5154            }
5155        }
5156        // `AS` followed by a $$-quoted body (lexer already
5157        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5158        // Token::String). AS is a reserved keyword (Token::As).
5159        if !matches!(self.peek(), Token::As) {
5160            return Err(self.err(alloc::format!(
5161                "expected AS before function body, got {:?}",
5162                self.peek()
5163            )));
5164        }
5165        self.advance();
5166        let body_text = match self.peek() {
5167            Token::String(s) => {
5168                let body = s.clone();
5169                self.advance();
5170                body
5171            }
5172            other => {
5173                return Err(self.err(alloc::format!(
5174                    "expected $$-quoted function body after AS, got {other:?}"
5175                )));
5176            }
5177        };
5178        // Trailing clauses — PG's other accepted position for both the
5179        // LANGUAGE and the attributes.
5180        loop {
5181            let before = self.pos;
5182            self.parse_function_attrs_into(&mut attrs)?;
5183            if language.is_none() {
5184                language = self.parse_optional_language()?;
5185            }
5186            if self.pos == before {
5187                break;
5188            }
5189        }
5190        let language = language.unwrap_or_else(|| String::from("sql"));
5191        // PL/pgSQL bodies get structure-parsed. Other languages
5192        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5193        // recognise) round-trip as Raw text — the executor errors
5194        // when invoked with a clear unsupported message.
5195        let body = if language.eq_ignore_ascii_case("plpgsql") {
5196            match parse_plpgsql_body(&body_text) {
5197                Ok(block) => FunctionBody::PlPgSql(block),
5198                // Best-effort: if the body parser doesn't yet
5199                // support a construct used inside, fall back to
5200                // raw — keeps `CREATE FUNCTION` itself working
5201                // (catalogue accepts), executor errors on
5202                // invocation only.
5203                Err(_) => FunctionBody::Raw(body_text),
5204            }
5205        } else {
5206            FunctionBody::Raw(body_text)
5207        };
5208        Ok(Statement::CreateFunction(CreateFunctionStatement {
5209            name,
5210            or_replace,
5211            args,
5212            returns,
5213            language,
5214            body,
5215            attrs,
5216        }))
5217    }
5218
5219    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5220    /// attribute clauses into `attrs`, stopping at the first token that
5221    /// is not one. Measured against PG 18.4, which accepts them in any
5222    /// order and on either side of the body.
5223    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5224        loop {
5225            let word = match self.peek() {
5226                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5227                // NOT LEAKPROOF — NOT is a reserved keyword token.
5228                Token::Not
5229                    if matches!(
5230                        self.tokens.get(self.pos + 1),
5231                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5232                    ) =>
5233                {
5234                    self.advance();
5235                    self.advance();
5236                    attrs.leakproof = false;
5237                    continue;
5238                }
5239                _ => return Ok(()),
5240            };
5241            match word.as_str() {
5242                "immutable" => {
5243                    self.advance();
5244                    attrs.volatility = FunctionVolatility::Immutable;
5245                }
5246                "stable" => {
5247                    self.advance();
5248                    attrs.volatility = FunctionVolatility::Stable;
5249                }
5250                "volatile" => {
5251                    self.advance();
5252                    attrs.volatility = FunctionVolatility::Volatile;
5253                }
5254                "strict" => {
5255                    self.advance();
5256                    attrs.strict = true;
5257                }
5258                "leakproof" => {
5259                    self.advance();
5260                    attrs.leakproof = true;
5261                }
5262                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5263                // spelled-out forms of STRICT and its opposite.
5264                "returns" | "called" => {
5265                    let strict = word == "returns";
5266                    let mut probe = self.pos + 1;
5267                    if strict {
5268                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5269                        // is not ours.
5270                        match self.tokens.get(probe) {
5271                            Some(Token::Null) => probe += 1,
5272                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5273                            _ => return Ok(()),
5274                        }
5275                    }
5276                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5277                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5278                    if !ok {
5279                        return Ok(());
5280                    }
5281                    probe += 1;
5282                    match self.tokens.get(probe) {
5283                        Some(Token::Null) => probe += 1,
5284                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5285                        _ => return Ok(()),
5286                    }
5287                    match self.tokens.get(probe) {
5288                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5289                        _ => return Ok(()),
5290                    }
5291                    self.pos = probe;
5292                    attrs.strict = strict;
5293                }
5294                "security" | "external" => {
5295                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5296                    let mut probe = self.pos + 1;
5297                    if word == "external" {
5298                        match self.tokens.get(probe) {
5299                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5300                                probe += 1;
5301                            }
5302                            _ => return Ok(()),
5303                        }
5304                    }
5305                    let definer = match self.tokens.get(probe) {
5306                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5307                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5308                        _ => return Ok(()),
5309                    };
5310                    self.pos = probe + 1;
5311                    attrs.security_definer = definer;
5312                }
5313                "parallel" => {
5314                    let level = match self.tokens.get(self.pos + 1) {
5315                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5316                            FunctionParallel::Safe
5317                        }
5318                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5319                            FunctionParallel::Restricted
5320                        }
5321                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5322                            FunctionParallel::Unsafe
5323                        }
5324                        _ => return Ok(()),
5325                    };
5326                    self.pos += 2;
5327                    attrs.parallel = level;
5328                }
5329                "cost" | "rows" => {
5330                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5331                        return Ok(());
5332                    };
5333                    self.pos += 2;
5334                    if word == "cost" {
5335                        attrs.cost = Some(n);
5336                    } else {
5337                        attrs.rows = Some(n);
5338                    }
5339                }
5340                _ => return Ok(()),
5341            }
5342        }
5343    }
5344
5345    /// The numeric literal at `idx`, if there is one.
5346    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5347        match self.tokens.get(idx)? {
5348            Token::Integer(n) => Some(*n as f64),
5349            Token::Float(f) => Some(*f),
5350            Token::Numeric(t) => t.parse::<f64>().ok(),
5351            _ => None,
5352        }
5353    }
5354
5355    /// Closing `)`-terminated argument list. v7.12.4 commonly
5356    /// sees the empty `()`; typed args round-trip but the
5357    /// executor (yet) doesn't invoke them.
5358    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5359    /// it away, which is what PG does with one on a function parameter.
5360    fn skip_type_modifier(&mut self) {
5361        if !matches!(self.peek(), Token::LParen) {
5362            return;
5363        }
5364        // Only a numeric modifier — anything else is not one, and eating
5365        // it would swallow real grammar.
5366        let mut i = self.pos + 1;
5367        let mut seen_number = false;
5368        loop {
5369            match self.tokens.get(i) {
5370                Some(Token::Integer(_)) => seen_number = true,
5371                Some(Token::Comma) => {}
5372                Some(Token::RParen) => break,
5373                _ => return,
5374            }
5375            i += 1;
5376        }
5377        if !seen_number {
5378            return;
5379        }
5380        while self.pos <= i {
5381            self.advance();
5382        }
5383    }
5384
5385    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5386        let mut args: Vec<FunctionArg> = Vec::new();
5387        if matches!(self.peek(), Token::RParen) {
5388            self.advance();
5389            return Ok(args);
5390        }
5391        loop {
5392            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5393            // a reserved token; OUT / INOUT are bare idents.
5394            let mode = if matches!(self.peek(), Token::In) {
5395                self.advance();
5396                FunctionArgMode::In
5397            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5398            {
5399                self.advance();
5400                FunctionArgMode::Out
5401            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5402            {
5403                self.advance();
5404                FunctionArgMode::InOut
5405            } else {
5406                FunctionArgMode::In
5407            };
5408            // Optional name. The next token is either a name
5409            // (followed by a type ident) or the type itself.
5410            // Disambiguate by peeking ahead: if the token after
5411            // the next ident is also an ident, we treat the
5412            // first as the name.
5413            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5414            // the comma or paren, then decide. Reading at most two of
5415            // them could not spell `x double precision` at all, and
5416            // silently mis-read the bare `double precision` as a
5417            // parameter named "double" — which is what made the same
5418            // signature key two different ways.
5419            let (name, ty_token) = {
5420                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5421                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5422                    words.push(self.expect_ident_like()?);
5423                }
5424                // v7.39 (round 344) — a length / precision modifier on the
5425                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5426                // accepts it and DROPS it — `pg_get_function_arguments`
5427                // reports plain `character varying` / `numeric`, measured on
5428                // 18.4 — but SPG raised `syntax error at or near "("`,
5429                // because the modifier's parens were never consumed.
5430                self.skip_type_modifier();
5431                // r1049 — `f(v bigint[])`. The array suffix parsed in
5432                // the column position, the cast position and (r1038)
5433                // the RETURNS position, but not here: the fifth
5434                // member of the same family, reported by sentori as
5435                // presumably the same code. It is now.
5436                let array_suffix = self.consume_array_suffix();
5437                let whole = words.join(" ");
5438                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5439                {
5440                    (Some(words[0].clone()), words[1..].join(" "))
5441                } else {
5442                    (None, whole)
5443                };
5444                ty_token.push_str(&array_suffix);
5445                (name, ty_token)
5446            };
5447            // Type — try to map to ColumnTypeName, else Raw.
5448            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5449                Some(t) => FunctionArgType::Typed(t),
5450                None => FunctionArgType::Raw(ty_token),
5451            };
5452            args.push(FunctionArg { mode, name, ty });
5453            match self.peek() {
5454                Token::Comma => {
5455                    self.advance();
5456                    continue;
5457                }
5458                Token::RParen => {
5459                    self.advance();
5460                    return Ok(args);
5461                }
5462                other => {
5463                    return Err(self.err(alloc::format!(
5464                        "expected , or ) in function arg list, got {other:?}"
5465                    )));
5466                }
5467            }
5468        }
5469    }
5470
5471    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5472        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5473        // function whose row shape is named inline.
5474        if matches!(self.peek(), Token::Table)
5475            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5476        {
5477            self.advance(); // TABLE
5478            self.advance(); // (
5479            let mut cols: Vec<String> = Vec::new();
5480            loop {
5481                let cname = self.expect_ident_like()?;
5482                let mut ty: Vec<String> = Vec::new();
5483                loop {
5484                    match self.peek() {
5485                        Token::Comma | Token::RParen | Token::Eof => break,
5486                        _ => {}
5487                    }
5488                    match self.advance() {
5489                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5490                        other => {
5491                            if let Some(w) = unreserved_keyword_text(&other) {
5492                                ty.push(w);
5493                            }
5494                        }
5495                    }
5496                }
5497                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5498                if matches!(self.peek(), Token::Comma) {
5499                    self.advance();
5500                } else {
5501                    break;
5502                }
5503            }
5504            if matches!(self.peek(), Token::RParen) {
5505                self.advance();
5506            }
5507            return Ok(FunctionReturn::Other(alloc::format!(
5508                "TABLE({})",
5509                cols.join(", ")
5510            )));
5511        }
5512        let ident = self.expect_ident_like()?;
5513        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5514        if ident.eq_ignore_ascii_case("setof") {
5515            let inner = self.expect_ident_like()?;
5516            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5517            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5518        }
5519        if ident.eq_ignore_ascii_case("trigger") {
5520            return Ok(FunctionReturn::Trigger);
5521        }
5522        if ident.eq_ignore_ascii_case("void") {
5523            return Ok(FunctionReturn::Void);
5524        }
5525        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5526        // RETURN position did not, so the `[` was a syntax error and the
5527        // whole migration stopped. sentori worked around it by returning
5528        // zero-padded text.
5529        let suffix = self.consume_array_suffix();
5530        if !suffix.is_empty() {
5531            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5532        }
5533        match map_type_ident_to_column_type_name(&ident) {
5534            Some(t) => Ok(FunctionReturn::Type(t)),
5535            None => Ok(FunctionReturn::Other(ident)),
5536        }
5537    }
5538
5539    /// Consume any `[]` / `[N]` array markers after a type name and give
5540    /// back their text. Empty when there are none.
5541    fn consume_array_suffix(&mut self) -> String {
5542        let mut out = String::new();
5543        while matches!(self.peek(), Token::LBracket) {
5544            self.advance();
5545            // `[N]` is accepted and, as in PG, the length is not enforced.
5546            if let Token::Integer(n) = self.peek().clone() {
5547                self.advance();
5548                out.push_str(&alloc::format!("[{n}]"));
5549            } else {
5550                out.push_str("[]");
5551            }
5552            if matches!(self.peek(), Token::RBracket) {
5553                self.advance();
5554            }
5555        }
5556        out
5557    }
5558
5559    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5560        match self.peek() {
5561            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5562                self.advance();
5563                let lang = self.expect_ident_like()?;
5564                Ok(Some(lang.to_ascii_lowercase()))
5565            }
5566            _ => Ok(None),
5567        }
5568    }
5569
5570    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5571    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5572    /// (expr)]*`. The `DOMAIN` keyword has already been
5573    /// consumed. PG allows the trailing constraints in any
5574    /// order; we approximate with a small loop.
5575    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5576        let name = self.expect_ident_like()?;
5577        // Optional `AS`.
5578        if matches!(self.peek(), Token::As) {
5579            self.advance();
5580        }
5581        // v7.39 (round 259) — keep the raw type NAME when the base is not
5582        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5583        // parent domain.
5584        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5585            self.parse_type_with_implied_flags()?;
5586        let mut default: Option<Expr> = None;
5587        let mut not_null = false;
5588        let mut checks: Vec<Expr> = Vec::new();
5589        loop {
5590            match self.peek() {
5591                Token::Default => {
5592                    if default.is_some() {
5593                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5594                    }
5595                    self.advance();
5596                    default = Some(self.parse_expr(0)?);
5597                }
5598                Token::Not => {
5599                    self.advance();
5600                    if !matches!(self.peek(), Token::Null) {
5601                        return Err(self.err(alloc::format!(
5602                            "expected NULL after NOT in DOMAIN, got {:?}",
5603                            self.peek()
5604                        )));
5605                    }
5606                    self.advance();
5607                    not_null = true;
5608                }
5609                Token::Null => {
5610                    self.advance();
5611                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5612                    // is the default-nullable marker (PG accepts it),
5613                    // but AFTER a NOT NULL it is a conflict PG refuses
5614                    // (`conflicting NULL/NOT NULL constraints`,
5615                    // PG18-measured); the old arm no-opped both ways.
5616                    if not_null {
5617                        return Err(self.err(alloc::string::String::from(
5618                            "conflicting NULL/NOT NULL constraints",
5619                        )));
5620                    }
5621                }
5622                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5623                    self.advance();
5624                    if !matches!(self.peek(), Token::LParen) {
5625                        return Err(self.err(alloc::format!(
5626                            "expected '(' after CHECK in DOMAIN, got {:?}",
5627                            self.peek()
5628                        )));
5629                    }
5630                    self.advance();
5631                    let expr = self.parse_expr(0)?;
5632                    if !matches!(self.peek(), Token::RParen) {
5633                        return Err(self.err(alloc::format!(
5634                            "expected ')' after CHECK expr, got {:?}",
5635                            self.peek()
5636                        )));
5637                    }
5638                    self.advance();
5639                    checks.push(expr);
5640                }
5641                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5642                // prefix on the constraint; we drop the name and
5643                // recurse into the constraint parsing.
5644                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5645                    self.advance();
5646                    let _ = self.expect_ident_like()?;
5647                }
5648                _ => break,
5649            }
5650        }
5651        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5652            name,
5653            base_type,
5654            base_domain: base_user_ref,
5655            default,
5656            not_null,
5657            checks,
5658        }))
5659    }
5660
5661    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5662    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5663    /// consumed.
5664    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5665        let name = self.expect_ident_like()?;
5666        // Required `AS`.
5667        if !matches!(self.peek(), Token::As) {
5668            return Err(self.err(alloc::format!(
5669                "expected AS after CREATE TYPE {name:?}, got {:?}",
5670                self.peek()
5671            )));
5672        }
5673        self.advance();
5674        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5675        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5676        // on the next token: `(` = composite, ident `ENUM` = enum.
5677        if matches!(self.peek(), Token::LParen) {
5678            self.advance();
5679            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5680            let mut field_user_types: Vec<Option<String>> = Vec::new();
5681            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5682            // is legal PG (an attribute-less composite; measured — the old
5683            // e2e note claimed PG requires at least one attribute).
5684            if matches!(self.peek(), Token::RParen) {
5685                self.advance();
5686                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5687                    name,
5688                    kind: crate::ast::TypeKind::Composite {
5689                        fields,
5690                        field_user_types,
5691                    },
5692                }));
5693            }
5694            loop {
5695                let field_name = self.expect_ident_like()?;
5696                // v7.39 (round 264) — keep the raw type name when it is not
5697                // a builtin: that is how a NESTED composite field records
5698                // which composite it holds.
5699                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5700                    self.parse_type_with_implied_flags()?;
5701                fields.push((field_name, field_type));
5702                field_user_types.push(field_user_ref);
5703                if matches!(self.peek(), Token::Comma) {
5704                    self.advance();
5705                    continue;
5706                }
5707                if matches!(self.peek(), Token::RParen) {
5708                    self.advance();
5709                    break;
5710                }
5711                return Err(self.err(alloc::format!(
5712                    "expected , or ) in composite field list, got {:?}",
5713                    self.peek()
5714                )));
5715            }
5716            if fields.is_empty() {
5717                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5718            }
5719            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5720                name,
5721                kind: crate::ast::TypeKind::Composite {
5722                    fields,
5723                    field_user_types,
5724                },
5725            }));
5726        }
5727        // Required `ENUM` ident.
5728        let kind_ident = match self.peek().clone() {
5729            Token::Ident(s) | Token::QuotedIdent(s) => s,
5730            other => {
5731                return Err(self.err(alloc::format!(
5732                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5733                )));
5734            }
5735        };
5736        if !kind_ident.eq_ignore_ascii_case("enum") {
5737            return Err(self.err(alloc::format!(
5738                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5739            )));
5740        }
5741        self.advance();
5742        if !matches!(self.peek(), Token::LParen) {
5743            return Err(self.err(alloc::format!(
5744                "expected '(' after ENUM, got {:?}",
5745                self.peek()
5746            )));
5747        }
5748        self.advance();
5749        let mut labels: Vec<String> = Vec::new();
5750        loop {
5751            match self.peek().clone() {
5752                Token::String(s) => {
5753                    self.advance();
5754                    labels.push(s);
5755                }
5756                other => {
5757                    return Err(
5758                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5759                    );
5760                }
5761            }
5762            if matches!(self.peek(), Token::Comma) {
5763                self.advance();
5764                continue;
5765            }
5766            if matches!(self.peek(), Token::RParen) {
5767                self.advance();
5768                break;
5769            }
5770            return Err(self.err(alloc::format!(
5771                "expected , or ) in ENUM label list, got {:?}",
5772                self.peek()
5773            )));
5774        }
5775        if labels.is_empty() {
5776            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5777        }
5778        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5779            name,
5780            kind: crate::ast::TypeKind::Enum { labels },
5781        }))
5782    }
5783
5784    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5785    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5786    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5787    /// consumed.
5788    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5789        let if_not_exists = self.parse_if_not_exists();
5790        let name = self.expect_ident_like()?;
5791        let mut columns: Vec<String> = Vec::new();
5792        if matches!(self.peek(), Token::LParen) {
5793            self.advance();
5794            loop {
5795                let c = self.expect_ident_like()?;
5796                columns.push(c);
5797                if matches!(self.peek(), Token::Comma) {
5798                    self.advance();
5799                    continue;
5800                }
5801                if matches!(self.peek(), Token::RParen) {
5802                    self.advance();
5803                    break;
5804                }
5805                return Err(self.err(alloc::format!(
5806                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5807                    self.peek()
5808                )));
5809            }
5810        }
5811        if !matches!(self.peek(), Token::As) {
5812            return Err(self.err(alloc::format!(
5813                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5814                self.peek()
5815            )));
5816        }
5817        self.advance();
5818        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5819        // CTEs only; the engine rejects data-modifying ones with PG's
5820        // message). A trailing `WITH [NO] DATA` can't START the body,
5821        // so WITH here heads the query.
5822        let body = if self.peek_is_with_kw() {
5823            self.advance();
5824            self.parse_nested_with_select()?
5825        } else {
5826            let body_stmt = self.parse_select_stmt()?;
5827            let Statement::Select(body) = body_stmt else {
5828                return Err(self.err(alloc::format!(
5829                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5830                )));
5831            };
5832            body
5833        };
5834        // Optional trailing `WITH [NO] DATA`.
5835        let with_data = self.parse_optional_with_data(true)?;
5836        Ok(Statement::CreateMaterializedView(
5837            crate::ast::CreateMaterializedViewStatement {
5838                temporary: false,
5839                name,
5840                if_not_exists,
5841                columns,
5842                body,
5843                with_data,
5844                as_plain_table: false,
5845            },
5846        ))
5847    }
5848
5849    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5850    /// `default_when_absent` is what to return if the tail is
5851    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5852    /// WITH DATA).
5853    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5854        let save = self.pos;
5855        // `WITH` is an Ident (not reserved in the lexer).
5856        let is_with = match self.peek() {
5857            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5858            _ => false,
5859        };
5860        if !is_with {
5861            return Ok(default_when_absent);
5862        }
5863        self.advance();
5864        // Optional `NO`.
5865        let mut with_data = true;
5866        let is_no = match self.peek() {
5867            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5868            _ => false,
5869        };
5870        if is_no {
5871            self.advance();
5872            with_data = false;
5873        }
5874        // Required `DATA` ident.
5875        let is_data = match self.peek() {
5876            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5877            _ => false,
5878        };
5879        if is_data {
5880            self.advance();
5881            Ok(with_data)
5882        } else {
5883            // Caller's WITH wasn't WITH-DATA — rewind so the outer
5884            // parser can interpret it.
5885            self.pos = save;
5886            Ok(default_when_absent)
5887        }
5888    }
5889
5890    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5891    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5892    /// All keyword prefixes have already been consumed; the flags
5893    /// say which were present.
5894    fn parse_create_view_after_keyword(
5895        &mut self,
5896        or_replace: bool,
5897        _materialized_unused: bool,
5898        temporary: bool,
5899    ) -> Result<Statement, ParseError> {
5900        let if_not_exists = self.parse_if_not_exists();
5901        let name = self.expect_ident_like()?;
5902        // Optional `(col, col, …)` rename list.
5903        let mut columns: Vec<String> = Vec::new();
5904        if matches!(self.peek(), Token::LParen) {
5905            self.advance();
5906            loop {
5907                let c = self.expect_ident_like()?;
5908                columns.push(c);
5909                if matches!(self.peek(), Token::Comma) {
5910                    self.advance();
5911                    continue;
5912                }
5913                if matches!(self.peek(), Token::RParen) {
5914                    self.advance();
5915                    break;
5916                }
5917                return Err(self.err(alloc::format!(
5918                    "expected , or ) in VIEW column list, got {:?}",
5919                    self.peek()
5920                )));
5921            }
5922        }
5923        // Required `AS`.
5924        if !matches!(self.peek(), Token::As) {
5925            return Err(self.err(alloc::format!(
5926                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5927                self.peek()
5928            )));
5929        }
5930        self.advance();
5931        // Body: a regular SELECT statement. v7.39 (round 151) — a
5932        // WITH-headed body is legal too (read-only CTEs only; the
5933        // engine rejects data-modifying ones with PG's message).
5934        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5935        // with the check-option clause, so WITH here heads the query.
5936        let body = if self.peek_is_with_kw() {
5937            self.advance();
5938            self.parse_nested_with_select()?
5939        } else {
5940            let body_stmt = self.parse_select_stmt()?;
5941            let Statement::Select(body) = body_stmt else {
5942                return Err(self.err(alloc::format!(
5943                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5944                )));
5945            };
5946            body
5947        };
5948        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5949        // The SELECT parser stops before a trailing WITH, so it lands here.
5950        let check_option = if matches!(self.peek(),
5951            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5952        {
5953            self.advance(); // WITH
5954            let opt = match self.peek() {
5955                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5956                    self.advance();
5957                    crate::ast::ViewCheckOption::Local
5958                }
5959                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5960                    self.advance();
5961                    crate::ast::ViewCheckOption::Cascaded
5962                }
5963                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5964                _ => crate::ast::ViewCheckOption::Cascaded,
5965            };
5966            if !matches!(self.peek(),
5967                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5968            {
5969                return Err(self.err(alloc::format!(
5970                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5971                    self.peek()
5972                )));
5973            }
5974            self.advance(); // CHECK
5975            if !matches!(self.peek(),
5976                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5977            {
5978                return Err(self.err(alloc::format!(
5979                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5980                    self.peek()
5981                )));
5982            }
5983            self.advance(); // OPTION
5984            Some(opt)
5985        } else {
5986            None
5987        };
5988        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5989            name,
5990            or_replace,
5991            if_not_exists,
5992            temporary,
5993            columns,
5994            body,
5995            check_option,
5996        }))
5997    }
5998
5999    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6000    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6001    /// consumed; `temporary` carries whether TEMPORARY was seen.
6002    fn parse_create_sequence_after_keyword(
6003        &mut self,
6004        temporary: bool,
6005    ) -> Result<Statement, ParseError> {
6006        let if_not_exists = self.parse_if_not_exists();
6007        let name = self.expect_ident_like()?;
6008        // Optional `AS data_type`.
6009        let data_type = if matches!(self.peek(), Token::As) {
6010            self.advance();
6011            Some(self.parse_sequence_data_type()?)
6012        } else {
6013            None
6014        };
6015        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6016        Ok(Statement::CreateSequence(
6017            crate::ast::CreateSequenceStatement {
6018                name,
6019                if_not_exists,
6020                temporary,
6021                data_type,
6022                options,
6023            },
6024        ))
6025    }
6026
6027    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6028    /// already been consumed; this is reached after `SEQUENCE`.
6029    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6030    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6031        use crate::ast::AlterDomainAction as A;
6032        let name = self.expect_ident_like()?;
6033        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6034        let kw = match self.peek() {
6035            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6036            Token::Drop => alloc::string::String::from("drop"),
6037            Token::Default => alloc::string::String::from("default"),
6038            other => {
6039                return Err(self.err(alloc::format!(
6040                    "expected an ALTER DOMAIN action, got {other:?}"
6041                )));
6042            }
6043        };
6044        let action = match kw.as_str() {
6045            "add" => {
6046                self.advance();
6047                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6048                {
6049                    self.advance();
6050                    Some(self.expect_ident_like()?)
6051                } else {
6052                    None
6053                };
6054                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6055                    return Err(self.err(alloc::format!(
6056                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6057                        self.peek()
6058                    )));
6059                }
6060                self.advance();
6061                if !matches!(self.peek(), Token::LParen) {
6062                    return Err(self.err("expected '(' after CHECK".into()));
6063                }
6064                self.advance();
6065                let check = self.parse_expr(0)?;
6066                if !matches!(self.peek(), Token::RParen) {
6067                    return Err(self.err("expected ')' after CHECK expression".into()));
6068                }
6069                self.advance();
6070                A::AddConstraint { name: cname, check }
6071            }
6072            "drop" => {
6073                self.advance();
6074                match self.peek() {
6075                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6076                        self.advance();
6077                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6078                        {
6079                            self.advance();
6080                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6081                            {
6082                                return Err(self.err("expected EXISTS after IF".into()));
6083                            }
6084                            self.advance();
6085                            true
6086                        } else {
6087                            false
6088                        };
6089                        let cn = self.expect_ident_like()?;
6090                        A::DropConstraint {
6091                            name: cn,
6092                            if_exists,
6093                        }
6094                    }
6095                    Token::Default => {
6096                        self.advance();
6097                        A::DropDefault
6098                    }
6099                    Token::Not => {
6100                        self.advance();
6101                        if !matches!(self.peek(), Token::Null) {
6102                            return Err(self.err("expected NULL after NOT".into()));
6103                        }
6104                        self.advance();
6105                        A::DropNotNull
6106                    }
6107                    other => {
6108                        return Err(self.err(alloc::format!(
6109                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6110                        )));
6111                    }
6112                }
6113            }
6114            "set" => {
6115                self.advance();
6116                match self.peek() {
6117                    Token::Default => {
6118                        self.advance();
6119                        A::SetDefault(self.parse_expr(0)?)
6120                    }
6121                    Token::Not => {
6122                        self.advance();
6123                        if !matches!(self.peek(), Token::Null) {
6124                            return Err(self.err("expected NULL after NOT".into()));
6125                        }
6126                        self.advance();
6127                        A::SetNotNull
6128                    }
6129                    other => {
6130                        return Err(self.err(alloc::format!(
6131                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6132                        )));
6133                    }
6134                }
6135            }
6136            "rename" => {
6137                self.advance();
6138                if !matches!(self.peek(), Token::To) {
6139                    return Err(self.err("expected TO after RENAME".into()));
6140                }
6141                self.advance();
6142                A::RenameTo(self.expect_ident_like()?)
6143            }
6144            other => {
6145                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6146            }
6147        };
6148        Ok(Statement::AlterDomain { name, action })
6149    }
6150
6151    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6152        let if_exists = self.parse_if_exists();
6153        let name = self.expect_ident_like()?;
6154        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6155        // the option list (PG allows only one or the other).
6156        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6157            self.advance();
6158            if matches!(self.peek(), Token::To) {
6159                self.advance();
6160            } else {
6161                self.expect_keyword_ident("to")?;
6162            }
6163            let new = self.expect_ident_like()?;
6164            return Ok(Statement::AlterSequence(
6165                crate::ast::AlterSequenceStatement {
6166                    name,
6167                    if_exists,
6168                    options: crate::ast::SequenceOptions::default(),
6169                    rename_to: Some(new),
6170                },
6171            ));
6172        }
6173        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6174        Ok(Statement::AlterSequence(
6175            crate::ast::AlterSequenceStatement {
6176                name,
6177                if_exists,
6178                options,
6179                rename_to: None,
6180            },
6181        ))
6182    }
6183
6184    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6185        let kw = self.expect_ident_like()?;
6186        match kw.to_ascii_lowercase().as_str() {
6187            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6188            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6189            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6190            other => Err(self.err(alloc::format!(
6191                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6192            ))),
6193        }
6194    }
6195
6196    fn parse_sequence_options(
6197        &mut self,
6198        allow_restart: bool,
6199    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6200        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6201        let mut opts = SequenceOptions::default();
6202        #[allow(clippy::while_let_loop)]
6203        loop {
6204            // Match an ident; stop at any non-ident token (sentinel,
6205            // semicolon, end of statement).
6206            let kw_lc = match self.peek() {
6207                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6208                _ => break,
6209            };
6210            match kw_lc.as_str() {
6211                "increment" => {
6212                    self.advance();
6213                    // Optional BY.
6214                    if self.peek_is_by() {
6215                        self.advance();
6216                    }
6217                    opts.increment = Some(self.expect_signed_int()?);
6218                }
6219                "minvalue" => {
6220                    self.advance();
6221                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6222                }
6223                "maxvalue" => {
6224                    self.advance();
6225                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6226                }
6227                "no" => {
6228                    self.advance();
6229                    let what = self.expect_ident_like()?;
6230                    match what.to_ascii_lowercase().as_str() {
6231                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6232                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6233                        "cycle" => opts.cycle = Some(false),
6234                        other => {
6235                            return Err(self.err(alloc::format!(
6236                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6237                            )));
6238                        }
6239                    }
6240                }
6241                "start" => {
6242                    self.advance();
6243                    // Optional WITH.
6244                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6245                        if s.eq_ignore_ascii_case("with"))
6246                    {
6247                        self.advance();
6248                    }
6249                    opts.start = Some(self.expect_signed_int()?);
6250                }
6251                "restart" if allow_restart => {
6252                    self.advance();
6253                    // Optional WITH n; bare RESTART means restart at START.
6254                    let mut with_val: Option<i64> = None;
6255                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6256                        if s.eq_ignore_ascii_case("with"))
6257                    {
6258                        self.advance();
6259                        with_val = Some(self.expect_signed_int()?);
6260                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6261                        with_val = Some(self.expect_signed_int()?);
6262                    }
6263                    opts.restart = Some(with_val);
6264                }
6265                "cache" => {
6266                    self.advance();
6267                    opts.cache = Some(self.expect_signed_int()?);
6268                }
6269                "cycle" => {
6270                    self.advance();
6271                    opts.cycle = Some(true);
6272                }
6273                "owned" => {
6274                    self.advance();
6275                    match self.peek() {
6276                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6277                            self.advance();
6278                        }
6279                        other => {
6280                            return Err(
6281                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6282                            );
6283                        }
6284                    }
6285                    // OWNED BY {NONE | tab.col}. Read just one ident
6286                    // (NOT expect_ident_like which would auto-strip
6287                    // a schema prefix and consume the `.col` we need).
6288                    let first = match self.advance() {
6289                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6290                        other => {
6291                            return Err(self.err(alloc::format!(
6292                                "expected identifier or NONE after OWNED BY, got {other:?}"
6293                            )));
6294                        }
6295                    };
6296                    if first.eq_ignore_ascii_case("none") {
6297                        opts.owned_by = Some(SequenceOwnedBy::None);
6298                    } else if matches!(self.peek(), Token::Dot) {
6299                        self.advance();
6300                        let second = match self.advance() {
6301                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6302                            other => {
6303                                return Err(self.err(alloc::format!(
6304                                    "expected column name after OWNED BY {first}., got {other:?}"
6305                                )));
6306                            }
6307                        };
6308                        // v7.17 dump-compat fix — pg_dump emits
6309                        // OWNED BY clauses as
6310                        // `schema.table.column` (three segments).
6311                        // If a third `.<ident>` follows, treat the
6312                        // first ident as schema (drop it; SPG is
6313                        // single-schema) and the middle / last
6314                        // pair as table.column. Otherwise it's
6315                        // the two-segment form table.column.
6316                        if matches!(self.peek(), Token::Dot) {
6317                            self.advance();
6318                            let third = match self.advance() {
6319                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6320                                other => {
6321                                    return Err(self.err(alloc::format!(
6322                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6323                                    )));
6324                                }
6325                            };
6326                            let _ = first; // schema prefix discarded
6327                            opts.owned_by = Some(SequenceOwnedBy::Column {
6328                                table: second,
6329                                column: third,
6330                            });
6331                        } else {
6332                            opts.owned_by = Some(SequenceOwnedBy::Column {
6333                                table: first,
6334                                column: second,
6335                            });
6336                        }
6337                    } else {
6338                        return Err(self.err(alloc::format!(
6339                            "expected table.column or NONE after OWNED BY, got {first:?}"
6340                        )));
6341                    }
6342                }
6343                _ => break,
6344            }
6345        }
6346        Ok(opts)
6347    }
6348
6349    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6350        let neg = if matches!(self.peek(), Token::Minus) {
6351            self.advance();
6352            true
6353        } else {
6354            false
6355        };
6356        match self.peek() {
6357            Token::Integer(n) => {
6358                let v = *n;
6359                self.advance();
6360                Ok(if neg { -v } else { v })
6361            }
6362            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6363        }
6364    }
6365
6366    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6367    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6368    /// clause is fully accepted and discarded — SPG always runs
6369    /// constraint checks immediately (single-writer model). The
6370    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6371    /// in either order (per the SQL spec they're independent),
6372    /// though pg_dump always emits them in the canonical
6373    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6374    /// Stops at the first token that isn't part of the clause.
6375    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6376        self.consume_deferrable_clauses_timed().map(|_| ())
6377    }
6378
6379    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6380    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6381    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6382    /// NOT DEFERRABLE and a circular-FK migration could not load.
6383    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6384        let mut deferrable = false;
6385        let mut initially_deferred = false;
6386        loop {
6387            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6388            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6389                self.advance();
6390                deferrable = true;
6391                if self.consume_optional_initially_clause()? {
6392                    initially_deferred = true;
6393                }
6394                continue;
6395            }
6396            // `NOT DEFERRABLE` — already worked pre-3.1.
6397            if matches!(self.peek(), Token::Not) {
6398                let look = self.tokens.get(self.pos + 1);
6399                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6400                    self.advance(); // NOT
6401                    self.advance(); // DEFERRABLE
6402                    deferrable = false;
6403                    initially_deferred = false;
6404                    let _ = self.consume_optional_initially_clause()?;
6405                    continue;
6406                }
6407                break;
6408            }
6409            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6410            // accepts this without a leading [NOT] DEFERRABLE
6411            // (the timing keyword alone). pg_dump occasionally
6412            // emits it on FK constraints that inherit timing.
6413            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6414                if self.consume_optional_initially_clause()? {
6415                    initially_deferred = true;
6416                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6417                    deferrable = true;
6418                }
6419                continue;
6420            }
6421            break;
6422        }
6423        Ok((deferrable, initially_deferred))
6424    }
6425
6426    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6427    /// next token is `INITIALLY`, consume it plus the required
6428    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6429    /// Returns true when the timing seen was `DEFERRED`.
6430    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6431        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6432            return Ok(false);
6433        }
6434        self.advance(); // INITIALLY
6435        match self.advance() {
6436            Token::Ident(s)
6437                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6438            {
6439                Ok(s.eq_ignore_ascii_case("deferred"))
6440            }
6441            other => Err(self.err(alloc::format!(
6442                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6443            ))),
6444        }
6445    }
6446
6447    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6448    /// in its entirety so the parser returns Empty without
6449    /// touching the runtime. The CREATE+PROCEDURE keywords are
6450    /// already consumed; this swallows everything from the
6451    /// procedure name through the matching `END`, including
6452    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6453    /// (DELIMITER `//` makes the script splitter forward the
6454    /// whole block as one statement), `@var` session-variable
6455    /// references, and the trailing terminator.
6456    ///
6457    /// Tracks nesting depth so:
6458    ///   BEGIN
6459    ///     IF cond THEN
6460    ///       BEGIN ... END;
6461    ///     END IF;
6462    ///   END
6463    /// terminates at the outer END.
6464    fn consume_mysql_routine_body(&mut self) {
6465        // Outer skeleton: name, (...), optional clauses, BEGIN
6466        // <body> END [;]. Scan for the first BEGIN — anything
6467        // before it is signature decoration we don't care about.
6468        // Once inside BEGIN, count up on BEGIN, down on END.
6469        let mut depth: i32 = 0;
6470        let mut started = false;
6471        loop {
6472            match self.peek().clone() {
6473                Token::Begin => {
6474                    self.advance();
6475                    depth += 1;
6476                    started = true;
6477                }
6478                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6479                    self.advance();
6480                    if started {
6481                        depth -= 1;
6482                        if depth <= 0 {
6483                            // Optional trailing ident (`END IF`,
6484                            // `END LOOP`, `END WHILE`, `END CASE`,
6485                            // `END label_name`) — eat the next
6486                            // ident if present so we don't
6487                            // mistake `END IF;` for the outer
6488                            // close.
6489                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6490                                // If the next token is one of the
6491                                // PL/SQL block-closer keywords,
6492                                // the END belongs to an inner
6493                                // block; bump depth back up.
6494                                let is_inner_close = matches!(
6495                                    self.peek(),
6496                                    Token::Ident(s) | Token::QuotedIdent(s)
6497                                        if matches!(
6498                                            s.to_ascii_lowercase().as_str(),
6499                                            "if" | "loop" | "while" | "case" | "repeat"
6500                                        )
6501                                );
6502                                if is_inner_close {
6503                                    self.advance();
6504                                    depth += 1;
6505                                    continue;
6506                                }
6507                            }
6508                            // Eat optional trailing `;`.
6509                            if matches!(self.peek(), Token::Semicolon) {
6510                                self.advance();
6511                            }
6512                            return;
6513                        }
6514                    }
6515                }
6516                Token::Eof => return,
6517                _ => {
6518                    self.advance();
6519                }
6520            }
6521        }
6522    }
6523
6524    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6525    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6526    ///
6527    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6528    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6529    ///   ident, or `ident @ ident-or-quoted-string` host form)
6530    /// * `SQL SECURITY {DEFINER|INVOKER}`
6531    ///
6532    /// Each clause may appear at most once but in any order.
6533    /// The hints are pure planner / permission metadata that
6534    /// SPG's view-rewrite engine handles uniformly; we accept
6535    /// and discard. Returns `Ok(())` once a non-clause token is
6536    /// peeked (the caller then checks for the `VIEW` keyword).
6537    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6538        loop {
6539            match self.peek().clone() {
6540                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6541                    self.advance(); // ALGORITHM
6542                    // Optional `=`. MySQL spec requires it but be
6543                    // generous.
6544                    if matches!(self.peek(), Token::Eq) {
6545                        self.advance();
6546                    }
6547                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6548                    // bare ident; unknown values still parse so
6549                    // future MySQL versions don't break.
6550                    if matches!(
6551                        self.peek(),
6552                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6553                    ) {
6554                        self.advance();
6555                    }
6556                }
6557                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6558                    self.advance(); // DEFINER
6559                    if matches!(self.peek(), Token::Eq) {
6560                        self.advance();
6561                    }
6562                    // User: quoted string, ident, OR ident @ host
6563                    // (host may itself be quoted or bare).
6564                    match self.peek().clone() {
6565                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6566                            self.advance();
6567                            // Optional `@host`.
6568                            if matches!(self.peek(), Token::At) {
6569                                self.advance();
6570                                if matches!(
6571                                    self.peek(),
6572                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6573                                ) {
6574                                    self.advance();
6575                                }
6576                            }
6577                        }
6578                        _ => {}
6579                    }
6580                }
6581                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6582                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6583                    // when followed by SECURITY — the dispatcher must
6584                    // not consume a bare `SQL` token (it's not a
6585                    // legal CREATE prefix on its own).
6586                    let save = self.pos;
6587                    self.advance(); // SQL
6588                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6589                        if s2.eq_ignore_ascii_case("security"))
6590                    {
6591                        self.advance(); // SECURITY
6592                        // DEFINER / INVOKER trailing ident.
6593                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6594                            self.advance();
6595                        }
6596                    } else {
6597                        // Not a SQL SECURITY clause — roll back and
6598                        // bail; the caller will error out cleanly.
6599                        self.pos = save;
6600                        return Ok(());
6601                    }
6602                }
6603                _ => return Ok(()),
6604            }
6605        }
6606    }
6607
6608    fn parse_if_not_exists(&mut self) -> bool {
6609        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6610        {
6611            let save = self.pos;
6612            self.advance();
6613            if matches!(self.peek(), Token::Not) {
6614                self.advance();
6615                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6616                {
6617                    self.advance();
6618                    return true;
6619                }
6620            }
6621            self.pos = save;
6622        }
6623        false
6624    }
6625
6626    fn parse_if_exists(&mut self) -> bool {
6627        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6628        {
6629            let save = self.pos;
6630            self.advance();
6631            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6632            {
6633                self.advance();
6634                return true;
6635            }
6636            self.pos = save;
6637        }
6638        false
6639    }
6640
6641    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6642    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6643    /// been consumed.
6644    fn parse_create_trigger_after_keyword(
6645        &mut self,
6646        or_replace: bool,
6647    ) -> Result<Statement, ParseError> {
6648        let name = self.expect_ident_like()?;
6649        let timing = {
6650            let ident = self.expect_ident_like()?;
6651            if ident.eq_ignore_ascii_case("before") {
6652                TriggerTiming::Before
6653            } else if ident.eq_ignore_ascii_case("after") {
6654                TriggerTiming::After
6655            } else if ident.eq_ignore_ascii_case("instead") {
6656                let next = self.expect_ident_like()?;
6657                if !next.eq_ignore_ascii_case("of") {
6658                    return Err(self.err(alloc::format!(
6659                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6660                    )));
6661                }
6662                TriggerTiming::InsteadOf
6663            } else {
6664                return Err(self.err(alloc::format!(
6665                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6666                )));
6667            }
6668        };
6669        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6670        // OR is a reserved keyword token (Token::Or), not an Ident.
6671        // v7.13.0 — after an UPDATE event we may optionally see
6672        // `OF col, col, …` (mailrs round-5 G7). Columns are
6673        // captured into `update_columns` once across the whole
6674        // events list; multiple `UPDATE OF` clauses are rejected.
6675        let mut events: Vec<TriggerEvent> = Vec::new();
6676        let mut update_columns: Vec<String> = Vec::new();
6677        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6678        events.push(first_ev);
6679        if !first_cols.is_empty() {
6680            update_columns = first_cols;
6681        }
6682        while matches!(self.peek(), Token::Or) {
6683            self.advance();
6684            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6685            events.push(ev);
6686            if !cols.is_empty() {
6687                if !update_columns.is_empty() {
6688                    return Err(
6689                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6690                    );
6691                }
6692                update_columns = cols;
6693            }
6694        }
6695        // ON <table>
6696        let tok = self.peek();
6697        let Token::On = tok else {
6698            return Err(self.err(alloc::format!(
6699                "expected ON after trigger events, got {tok:?}"
6700            )));
6701        };
6702        self.advance();
6703        let table = self.expect_ident_like()?;
6704        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6705        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6706        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6707        // the trigger as a plain AFTER trigger (correct for every non-deferred
6708        // use; deferral timing is not yet honoured).
6709        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6710            if s.eq_ignore_ascii_case("from"))
6711        {
6712            self.advance();
6713            let _reftable = self.expect_ident_like()?;
6714        }
6715        self.consume_optional_deferrable_clauses()?;
6716        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6717        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6718        // idents.
6719        if !matches!(self.peek(), Token::For) {
6720            return Err(self.err(alloc::format!(
6721                "expected FOR EACH ROW / STATEMENT, got {:?}",
6722                self.peek()
6723            )));
6724        }
6725        self.advance();
6726        let for_each = {
6727            let e = self.expect_ident_like()?;
6728            if !e.eq_ignore_ascii_case("each") {
6729                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6730            }
6731            let unit = self.expect_ident_like()?;
6732            if unit.eq_ignore_ascii_case("row") {
6733                TriggerForEach::Row
6734            } else if unit.eq_ignore_ascii_case("statement") {
6735                TriggerForEach::Statement
6736            } else {
6737                return Err(self.err(alloc::format!(
6738                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6739                )));
6740            }
6741        };
6742        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6743        let when_condition = if matches!(self.peek(),
6744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6745        {
6746            self.advance();
6747            Some(self.parse_paren_expr("WHEN")?)
6748        } else {
6749            None
6750        };
6751        // EXECUTE FUNCTION/PROCEDURE name(...)
6752        let exec = self.expect_ident_like()?;
6753        if !exec.eq_ignore_ascii_case("execute") {
6754            return Err(self.err(alloc::format!(
6755                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6756            )));
6757        }
6758        let fn_or_proc = self.expect_ident_like()?;
6759        if !(fn_or_proc.eq_ignore_ascii_case("function")
6760            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6761        {
6762            return Err(self.err(alloc::format!(
6763                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6764            )));
6765        }
6766        let function = self.expect_ident_like()?;
6767        // Optional empty arg list `()`.
6768        if matches!(self.peek(), Token::LParen) {
6769            self.advance();
6770            if !matches!(self.peek(), Token::RParen) {
6771                return Err(self.err(alloc::format!(
6772                    "v7.12.4 trigger function calls take no args; got {:?}",
6773                    self.peek()
6774                )));
6775            }
6776            self.advance();
6777        }
6778        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6779            name,
6780            or_replace,
6781            timing,
6782            events,
6783            table,
6784            for_each,
6785            function,
6786            update_columns,
6787            when_condition,
6788        }))
6789    }
6790
6791    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6792    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6793    fn parse_create_rule_after_keyword(
6794        &mut self,
6795        or_replace: bool,
6796    ) -> Result<Statement, ParseError> {
6797        let name = self.expect_ident_like()?;
6798        if !matches!(self.peek(), Token::As) {
6799            return Err(self.err(alloc::format!(
6800                "expected AS in CREATE RULE, got {:?}",
6801                self.peek()
6802            )));
6803        }
6804        self.advance();
6805        if !matches!(self.peek(), Token::On) {
6806            return Err(self.err(alloc::format!(
6807                "expected ON in CREATE RULE, got {:?}",
6808                self.peek()
6809            )));
6810        }
6811        self.advance();
6812        let event = self.parse_rule_event()?;
6813        if !matches!(self.peek(), Token::To)
6814            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6815        {
6816            return Err(self.err(alloc::format!(
6817                "expected TO after rule event, got {:?}",
6818                self.peek()
6819            )));
6820        }
6821        self.advance();
6822        let table = self.expect_ident_like()?;
6823        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6824        let when_condition = if matches!(self.peek(), Token::Where) {
6825            self.advance();
6826            Some(self.parse_expr(0)?)
6827        } else {
6828            None
6829        };
6830        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6831        {
6832            return Err(self.err(alloc::format!(
6833                "expected DO in CREATE RULE, got {:?}",
6834                self.peek()
6835            )));
6836        }
6837        self.advance();
6838        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6839        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6840        {
6841            self.advance();
6842            true
6843        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6844            self.advance();
6845            false
6846        } else {
6847            false
6848        };
6849        // `NOTHING` | `( cmd; … )` | `cmd`.
6850        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6851        {
6852            self.advance();
6853            Vec::new()
6854        } else if matches!(self.peek(), Token::LParen) {
6855            self.advance();
6856            let mut cmds = Vec::new();
6857            loop {
6858                cmds.push(self.parse_one_statement()?);
6859                if matches!(self.peek(), Token::Semicolon) {
6860                    self.advance();
6861                    if matches!(self.peek(), Token::RParen) {
6862                        break;
6863                    }
6864                    continue;
6865                }
6866                break;
6867            }
6868            if !matches!(self.peek(), Token::RParen) {
6869                return Err(self.err(alloc::format!(
6870                    "expected ) closing the CREATE RULE command list, got {:?}",
6871                    self.peek()
6872                )));
6873            }
6874            self.advance();
6875            cmds
6876        } else {
6877            alloc::vec![self.parse_one_statement()?]
6878        };
6879        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6880            name,
6881            or_replace,
6882            event,
6883            table,
6884            instead,
6885            when_condition,
6886            commands,
6887        }))
6888    }
6889
6890    /// v7.39 (round 139) — a rule event keyword → uppercase string.
6891    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6892        if matches!(self.peek(), Token::Insert) {
6893            self.advance();
6894            return Ok(alloc::string::String::from("INSERT"));
6895        }
6896        if matches!(self.peek(), Token::Select) {
6897            self.advance();
6898            return Ok(alloc::string::String::from("SELECT"));
6899        }
6900        match self.peek() {
6901            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6902                self.advance();
6903                Ok(alloc::string::String::from("UPDATE"))
6904            }
6905            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6906                self.advance();
6907                Ok(alloc::string::String::from("DELETE"))
6908            }
6909            other => Err(self.err(alloc::format!(
6910                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6911            ))),
6912        }
6913    }
6914
6915    /// v7.13.0 — parse one trigger event, then optionally consume
6916    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6917    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6918    fn parse_trigger_event_with_optional_of(
6919        &mut self,
6920    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6921        let ev = self.parse_trigger_event()?;
6922        if !matches!(ev, TriggerEvent::Update) {
6923            return Ok((ev, Vec::new()));
6924        }
6925        // `OF` is a bare ident.
6926        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6927            return Ok((ev, Vec::new()));
6928        }
6929        self.advance(); // OF
6930        let mut cols: Vec<String> = Vec::new();
6931        loop {
6932            cols.push(self.expect_ident_like()?);
6933            if matches!(self.peek(), Token::Comma) {
6934                self.advance();
6935                continue;
6936            }
6937            break;
6938        }
6939        if cols.is_empty() {
6940            return Err(
6941                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6942            );
6943        }
6944        Ok((ev, cols))
6945    }
6946
6947    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6948    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6949    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6950    /// inside the body.
6951    /// Called by [`parse_plpgsql_body`] after the body's tokens
6952    /// have been lexed into this temporary parser.
6953    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6954        // v7.12.6 — optional DECLARE prelude.
6955        let declarations = if matches!(
6956            self.peek(),
6957            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6958        ) {
6959            self.advance();
6960            self.parse_plpgsql_declare_block()?
6961        } else {
6962            Vec::new()
6963        };
6964        // BEGIN keyword (PL/pgSQL — distinct from the SQL
6965        // `BEGIN` transaction-start, but we can reuse the
6966        // reserved Token::Begin since the body is a separate
6967        // lex/parse context).
6968        if !matches!(self.peek(), Token::Begin) {
6969            return Err(self.err(alloc::format!(
6970                "expected BEGIN at start of plpgsql block, got {:?}",
6971                self.peek()
6972            )));
6973        }
6974        self.advance();
6975        let statements = self.parse_plpgsql_stmt_list_until_end()?;
6976        // v7.37.20 (20.10) — optional EXCEPTION clause between the
6977        // body's last statement and the trailing END. When present
6978        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6979        // arms terminated by END.
6980        let exception_handlers = if matches!(
6981            self.peek(),
6982            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6983        ) {
6984            self.advance();
6985            self.parse_plpgsql_exception_handlers()?
6986        } else {
6987            Vec::new()
6988        };
6989        Ok(PlPgSqlBlock {
6990            declarations,
6991            statements,
6992            exception_handlers,
6993        })
6994    }
6995
6996    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6997    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6998    fn parse_plpgsql_exception_handlers(
6999        &mut self,
7000    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7001        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7002        loop {
7003            // Stop at END — the block-level trailing END LOOP / END;
7004            // is handled by the caller.
7005            if matches!(
7006                self.peek(),
7007                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7008            ) {
7009                return Ok(out);
7010            }
7011            // WHEN <cond> [OR <cond>]* THEN <body>
7012            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7013            {
7014                return Err(self.err(alloc::format!(
7015                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7016                    self.peek()
7017                )));
7018            }
7019            self.advance();
7020            let mut conditions: Vec<String> = Vec::new();
7021            conditions.push(self.expect_ident_like()?);
7022            while matches!(self.peek(), Token::Or) {
7023                self.advance();
7024                conditions.push(self.expect_ident_like()?);
7025            }
7026            let then_kw = self.expect_ident_like()?;
7027            if !then_kw.eq_ignore_ascii_case("then") {
7028                return Err(self.err(alloc::format!(
7029                    "expected THEN after WHEN condition list, got {then_kw:?}"
7030                )));
7031            }
7032            let body = self.parse_plpgsql_stmt_list_until_end()?;
7033            out.push(crate::ast::ExceptionHandler { conditions, body });
7034        }
7035    }
7036
7037    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7038    /// prelude. Caller has already consumed `DECLARE`. We stop
7039    /// reading entries when we hit `BEGIN`.
7040    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7041        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7042        loop {
7043            if matches!(self.peek(), Token::Begin) {
7044                return Ok(out);
7045            }
7046            let name = self.expect_ident_like()?;
7047            // v7.37.20 (20.7) — type inference: if the next token is
7048            // `:=` or `=` (no explicit type), infer from the default
7049            // expression. Otherwise the ident that follows is the
7050            // declared type.
7051            //
7052            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7053            // (PG-standard). SPG parse-accepts and treats identically
7054            // to inference — the eventual runtime value determines
7055            // the local's type, which is faithful to how SPG handles
7056            // untyped locals today (see 20.7). Full compile-time
7057            // catalog lookup queues with v7.40 PL/pgSQL epic.
7058            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7059                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7060                // downstream declaration walker to type the local by
7061                // the runtime type of the default expression.
7062                FunctionArgType::Raw("_infer_".into())
7063            } else {
7064                let ty_token = self.expect_ident_like()?;
7065                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7066                // consume optional `.<ident>` qualifier + `%<KW>`
7067                // suffix. Both qualifier and suffix map to _infer_.
7068                if matches!(self.peek(), Token::Dot) {
7069                    self.advance();
7070                    let _ = self.expect_ident_like()?;
7071                }
7072                if matches!(self.peek(), Token::Percent) {
7073                    self.advance();
7074                    // Consume the trailing TYPE / ROWTYPE ident.
7075                    let _ = self.expect_ident_like()?;
7076                    FunctionArgType::Raw("_infer_".into())
7077                } else {
7078                    match map_type_ident_to_column_type_name(&ty_token) {
7079                        Some(t) => FunctionArgType::Typed(t),
7080                        None => FunctionArgType::Raw(ty_token),
7081                    }
7082                }
7083            };
7084            let default = match self.peek() {
7085                Token::ColonEq => {
7086                    self.advance();
7087                    Some(self.parse_expr(0)?)
7088                }
7089                Token::Eq => {
7090                    // PL/pgSQL also accepts `=` for the
7091                    // DECLARE default (PG treats them the same
7092                    // in this position).
7093                    self.advance();
7094                    Some(self.parse_expr(0)?)
7095                }
7096                _ => None,
7097            };
7098            // Mandatory `;` between declarations.
7099            if !matches!(self.peek(), Token::Semicolon) {
7100                return Err(self.err(alloc::format!(
7101                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7102                    self.peek()
7103                )));
7104            }
7105            self.advance();
7106            out.push(PlPgSqlDeclare { name, ty, default });
7107        }
7108    }
7109
7110    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7111    /// the terminating `END;` (or `END IF;` etc — handled by the
7112    /// per-construct sub-parsers). Used by both the outer block
7113    /// and the IF/ELSE branch bodies.
7114    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7115        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7116        loop {
7117            // Allow trailing semicolons + END.
7118            while matches!(self.peek(), Token::Semicolon) {
7119                self.advance();
7120            }
7121            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7122            if matches!(
7123                self.peek(),
7124                Token::Ident(s) | Token::QuotedIdent(s)
7125                    if s.eq_ignore_ascii_case("end")
7126                        || s.eq_ignore_ascii_case("else")
7127                        || s.eq_ignore_ascii_case("elsif")
7128                        || s.eq_ignore_ascii_case("elseif")
7129                        || s.eq_ignore_ascii_case("exception")
7130                        || s.eq_ignore_ascii_case("when")
7131            ) {
7132                return Ok(statements);
7133            }
7134            // Otherwise: one statement, then expect `;` or
7135            // a block-terminator keyword.
7136            let stmt = self.parse_plpgsql_stmt()?;
7137            statements.push(stmt);
7138            match self.peek() {
7139                Token::Semicolon => {
7140                    self.advance();
7141                }
7142                Token::Ident(s) | Token::QuotedIdent(s)
7143                    if s.eq_ignore_ascii_case("end")
7144                        || s.eq_ignore_ascii_case("else")
7145                        || s.eq_ignore_ascii_case("elsif")
7146                        || s.eq_ignore_ascii_case("elseif")
7147                        || s.eq_ignore_ascii_case("exception")
7148                        || s.eq_ignore_ascii_case("when") =>
7149                {
7150                    // Final statement of the block without `;`.
7151                }
7152                other => {
7153                    return Err(self.err(alloc::format!(
7154                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7155                    )));
7156                }
7157            }
7158        }
7159    }
7160
7161    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7162        // RETURN keyword?
7163        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7164        {
7165            self.advance();
7166            return self.parse_plpgsql_return();
7167        }
7168        // v7.12.6 — IF block.
7169        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7170        {
7171            self.advance();
7172            return self.parse_plpgsql_if();
7173        }
7174        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7175        // Detected by peeking that token pos+3 is Ident("execute").
7176        if matches!(self.peek(), Token::For)
7177            && matches!(
7178                self.tokens.get(self.pos + 1),
7179                Some(Token::Ident(_) | Token::QuotedIdent(_))
7180            )
7181            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7182            && matches!(
7183                self.tokens.get(self.pos + 3),
7184                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7185            )
7186        {
7187            self.advance(); // FOR
7188            let var = self.expect_ident_like()?;
7189            self.advance(); // IN
7190            self.advance(); // EXECUTE
7191            // Prescan for LOOP at paren depth 0 so parse_expr stops
7192            // before the LOOP keyword (same trick as the bare-SELECT
7193            // ForQuery arm).
7194            let mut depth: i32 = 0;
7195            let mut loop_pos: Option<usize> = None;
7196            let mut scan = self.pos;
7197            while scan < self.tokens.len() {
7198                match self.tokens.get(scan) {
7199                    Some(Token::LParen) => depth += 1,
7200                    Some(Token::RParen) => depth -= 1,
7201                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7202                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7203                    {
7204                        loop_pos = Some(scan);
7205                        break;
7206                    }
7207                    _ => {}
7208                }
7209                scan += 1;
7210            }
7211            let loop_pos = loop_pos.ok_or_else(|| {
7212                self.err(alloc::format!(
7213                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7214                ))
7215            })?;
7216            let saved_loop = self.tokens[loop_pos].clone();
7217            self.tokens[loop_pos] = Token::Semicolon;
7218            let expr_result = self.parse_expr(0);
7219            self.tokens[loop_pos] = saved_loop;
7220            let sql_expr = expr_result?;
7221            let loop_kw = self.expect_ident_like()?;
7222            if !loop_kw.eq_ignore_ascii_case("loop") {
7223                return Err(self.err(alloc::format!(
7224                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7225                )));
7226            }
7227            let body = self.parse_plpgsql_stmt_list_until_end()?;
7228            let end_kw = self.expect_ident_like()?;
7229            if !end_kw.eq_ignore_ascii_case("end") {
7230                return Err(self.err(alloc::format!(
7231                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7232                )));
7233            }
7234            let loop_kw2 = self.expect_ident_like()?;
7235            if !loop_kw2.eq_ignore_ascii_case("loop") {
7236                return Err(self.err(alloc::format!(
7237                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7238                )));
7239            }
7240            return Ok(PlPgSqlStmt::ForExecute {
7241                var,
7242                sql_expr,
7243                body,
7244            });
7245        }
7246        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7247        //
7248        // Two syntactic forms:
7249        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7250        //   FOR var IN (SELECT ...) LOOP ...
7251        //
7252        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7253        // the trailing `LOOP` keyword as a table alias, we prescan
7254        // forward to find LOOP at paren depth 0, splice a fake
7255        // Semicolon at that position (so SELECT parses cleanly),
7256        // then re-splice LOOP back in.
7257        //
7258        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7259        // LOOP directly — no scan required.
7260        if matches!(self.peek(), Token::For)
7261            && matches!(
7262                self.tokens.get(self.pos + 1),
7263                Some(Token::Ident(_) | Token::QuotedIdent(_))
7264            )
7265            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7266            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7267                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7268        {
7269            self.advance(); // FOR
7270            let var = self.expect_ident_like()?;
7271            // IN
7272            self.advance();
7273            let query = if matches!(self.peek(), Token::LParen) {
7274                // Paren-wrapped SELECT.
7275                self.advance();
7276                let inner = self.parse_select_stmt()?;
7277                let Statement::Select(q) = inner else {
7278                    return Err(self.err(alloc::format!(
7279                        "expected SELECT inside (…), got {:?}",
7280                        self.peek()
7281                    )));
7282                };
7283                if !matches!(self.peek(), Token::RParen) {
7284                    return Err(self.err(alloc::format!(
7285                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7286                        self.peek()
7287                    )));
7288                }
7289                self.advance();
7290                q
7291            } else {
7292                // Bare SELECT: prescan to find the LOOP boundary.
7293                let mut depth: i32 = 0;
7294                let mut loop_pos: Option<usize> = None;
7295                let mut scan = self.pos;
7296                while scan < self.tokens.len() {
7297                    match self.tokens.get(scan) {
7298                        Some(Token::LParen) => depth += 1,
7299                        Some(Token::RParen) => depth -= 1,
7300                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7301                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7302                        {
7303                            loop_pos = Some(scan);
7304                            break;
7305                        }
7306                        _ => {}
7307                    }
7308                    scan += 1;
7309                }
7310                let loop_pos = loop_pos.ok_or_else(|| {
7311                    self.err(alloc::format!(
7312                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7313                    ))
7314                })?;
7315                // Swap the LOOP token with a synthetic Semicolon so
7316                // parse_select_stmt stops there, then restore afterward.
7317                let saved_loop = self.tokens[loop_pos].clone();
7318                self.tokens[loop_pos] = Token::Semicolon;
7319                let parse_result = self.parse_select_stmt();
7320                self.tokens[loop_pos] = saved_loop;
7321                let inner = parse_result?;
7322                let Statement::Select(q) = inner else {
7323                    return Err(self.err(alloc::format!(
7324                        "expected SELECT after FOR <var> IN, got {:?}",
7325                        self.peek()
7326                    )));
7327                };
7328                q
7329            };
7330            let loop_kw = self.expect_ident_like()?;
7331            if !loop_kw.eq_ignore_ascii_case("loop") {
7332                return Err(self.err(alloc::format!(
7333                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7334                )));
7335            }
7336            let body = self.parse_plpgsql_stmt_list_until_end()?;
7337            let end_kw = self.expect_ident_like()?;
7338            if !end_kw.eq_ignore_ascii_case("end") {
7339                return Err(self.err(alloc::format!(
7340                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7341                )));
7342            }
7343            let loop_kw2 = self.expect_ident_like()?;
7344            if !loop_kw2.eq_ignore_ascii_case("loop") {
7345                return Err(self.err(alloc::format!(
7346                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7347                )));
7348            }
7349            return Ok(PlPgSqlStmt::ForQuery {
7350                var,
7351                query: Box::new(query),
7352                body,
7353            });
7354        }
7355        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7356        // FOR is a reserved keyword token (Token::For).
7357        if matches!(self.peek(), Token::For)
7358            && matches!(
7359                self.tokens.get(self.pos + 1),
7360                Some(Token::Ident(_) | Token::QuotedIdent(_))
7361            )
7362            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7363        {
7364            self.advance(); // FOR
7365            let var = self.expect_ident_like()?;
7366            if !matches!(self.peek(), Token::In) {
7367                return Err(self.err(alloc::format!(
7368                    "expected IN after FOR <var>, got {:?}",
7369                    self.peek()
7370                )));
7371            }
7372            self.advance();
7373            let reverse = matches!(
7374                self.peek(),
7375                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7376            );
7377            if reverse {
7378                self.advance();
7379            }
7380            let start = self.parse_expr(0)?;
7381            if !matches!(self.peek(), Token::DotDot) {
7382                return Err(self.err(alloc::format!(
7383                    "expected '..' between FOR loop bounds, got {:?}",
7384                    self.peek()
7385                )));
7386            }
7387            self.advance();
7388            let end = self.parse_expr(0)?;
7389            let loop_kw = self.expect_ident_like()?;
7390            if !loop_kw.eq_ignore_ascii_case("loop") {
7391                return Err(self.err(alloc::format!(
7392                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7393                )));
7394            }
7395            let body = self.parse_plpgsql_stmt_list_until_end()?;
7396            let end_kw = self.expect_ident_like()?;
7397            if !end_kw.eq_ignore_ascii_case("end") {
7398                return Err(self.err(alloc::format!(
7399                    "expected END LOOP after FOR body, got {end_kw:?}"
7400                )));
7401            }
7402            let loop_kw2 = self.expect_ident_like()?;
7403            if !loop_kw2.eq_ignore_ascii_case("loop") {
7404                return Err(self.err(alloc::format!(
7405                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7406                )));
7407            }
7408            return Ok(PlPgSqlStmt::ForRange {
7409                var,
7410                start,
7411                end,
7412                reverse,
7413                body,
7414            });
7415        }
7416        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7417        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7418        {
7419            self.advance();
7420            let body = self.parse_plpgsql_stmt_list_until_end()?;
7421            let end_kw = self.expect_ident_like()?;
7422            if !end_kw.eq_ignore_ascii_case("end") {
7423                return Err(self.err(alloc::format!(
7424                    "expected END LOOP after LOOP body, got {end_kw:?}"
7425                )));
7426            }
7427            let loop_kw = self.expect_ident_like()?;
7428            if !loop_kw.eq_ignore_ascii_case("loop") {
7429                return Err(self.err(alloc::format!(
7430                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7431                )));
7432            }
7433            return Ok(PlPgSqlStmt::Loop { body });
7434        }
7435        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7436        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7437        {
7438            self.advance();
7439            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7440            {
7441                self.advance();
7442                Some(self.parse_expr(0)?)
7443            } else {
7444                None
7445            };
7446            return Ok(PlPgSqlStmt::Exit { when });
7447        }
7448        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7449        // already-parsed Statement or a runtime-computed SQL string.
7450        // The disambiguator vs the extended-query-protocol `EXECUTE
7451        // <stmt_name>` (which is a top-level Statement, not a
7452        // plpgsql line) is that inside a DO block / trigger body the
7453        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7454        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7455        {
7456            self.advance();
7457            let sql = self.parse_expr(0)?;
7458            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7459        }
7460        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7461        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7462        {
7463            self.advance();
7464            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7465            {
7466                self.advance();
7467                Some(self.parse_expr(0)?)
7468            } else {
7469                None
7470            };
7471            return Ok(PlPgSqlStmt::Continue { when });
7472        }
7473        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7474        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7475        {
7476            self.advance();
7477            let condition = self.parse_expr(0)?;
7478            let loop_kw = self.expect_ident_like()?;
7479            if !loop_kw.eq_ignore_ascii_case("loop") {
7480                return Err(self.err(alloc::format!(
7481                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7482                )));
7483            }
7484            let body = self.parse_plpgsql_stmt_list_until_end()?;
7485            // Expect END LOOP.
7486            let end_kw = self.expect_ident_like()?;
7487            if !end_kw.eq_ignore_ascii_case("end") {
7488                return Err(self.err(alloc::format!(
7489                    "expected END LOOP after WHILE body, got {end_kw:?}"
7490                )));
7491            }
7492            let loop_kw2 = self.expect_ident_like()?;
7493            if !loop_kw2.eq_ignore_ascii_case("loop") {
7494                return Err(self.err(alloc::format!(
7495                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7496                )));
7497            }
7498            return Ok(PlPgSqlStmt::While { condition, body });
7499        }
7500        // v7.12.6 — RAISE.
7501        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7502        {
7503            self.advance();
7504            return self.parse_plpgsql_raise();
7505        }
7506        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7507        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7508        {
7509            self.advance();
7510            let condition = self.parse_expr(0)?;
7511            let message = if matches!(self.peek(), Token::Comma) {
7512                self.advance();
7513                Some(self.parse_expr(0)?)
7514            } else {
7515                None
7516            };
7517            return Ok(PlPgSqlStmt::Assert { condition, message });
7518        }
7519        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7520        //   "PERFORM is equivalent to SELECT but discards the
7521        //    result." Side effects (function calls, RAISE inside
7522        //    SQL functions, etc.) still execute. We desugar to
7523        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7524        //    existing embedded-statement path handles execution +
7525        //    result-discard cleanly. The result is naturally
7526        //    discarded because EmbeddedSql doesn't propagate row
7527        //    sets back to the plpgsql interpreter.
7528        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7529        {
7530            self.advance();
7531            // Splice a synthetic Token::Select into the stream at
7532            // the current position so parse_select_stmt parses the
7533            // remainder as a normal SELECT body. Token-stream
7534            // surgery mirrors the try_parse_plpgsql_select_into
7535            // pattern used for SELECT … INTO desugaring.
7536            self.tokens.insert(self.pos, Token::Select);
7537            let select = self.parse_select_stmt()?;
7538            let Statement::Select(s) = select else {
7539                return Err(self.err(alloc::format!(
7540                    "expected SELECT body after PERFORM, got {:?}",
7541                    self.peek()
7542                )));
7543            };
7544            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7545        }
7546        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7547        // plpgsql-specific shape (mailrs round-10 migrate-042).
7548        // PG's SELECT INTO at top-level SQL would CREATE a new
7549        // table; inside plpgsql it ASSIGNS the query result to
7550        // a local variable. We detect the INTO at paren-depth
7551        // 0 between SELECT and the statement boundary; if
7552        // found, split the token stream into "pre-INTO
7553        // projection" + "var" + "post-INTO FROM/WHERE…" and
7554        // rebuild as a SelectInto with a regular SELECT body
7555        // (no INTO clause).
7556        if matches!(self.peek(), Token::Select)
7557            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7558        {
7559            return Ok(PlPgSqlStmt::SelectInto {
7560                var: var_name,
7561                body: Box::new(select_body),
7562            });
7563        }
7564        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7565        // SELECT can appear directly inside a trigger body; we
7566        // recurse into the regular Statement parser, which will
7567        // stop at the trailing `;` (which our caller then
7568        // consumes).
7569        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7570        // also embed ALTER / CREATE / DROP statements; route
7571        // those through the same parser so the DO body parses
7572        // cleanly.
7573        if matches!(self.peek(), Token::Insert)
7574            || matches!(self.peek(), Token::Select)
7575            || matches!(self.peek(), Token::Create)
7576            || matches!(self.peek(), Token::Drop)
7577            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7578                if s.eq_ignore_ascii_case("update")
7579                    || s.eq_ignore_ascii_case("delete")
7580                    || s.eq_ignore_ascii_case("alter"))
7581        {
7582            let stmt = self.parse_one_statement()?;
7583            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7584        }
7585        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7586        // followed by `:=` and an expression.
7587        let target = self.parse_plpgsql_assign_target()?;
7588        // PL/pgSQL assignment uses `:=`. The lexer represents
7589        // this as a colon followed by `=`; check both shapes.
7590        match self.peek() {
7591            Token::ColonEq => {
7592                self.advance();
7593            }
7594            Token::Colon => {
7595                self.advance();
7596                if !matches!(self.peek(), Token::Eq) {
7597                    return Err(self.err(alloc::format!(
7598                        "expected := after plpgsql assign target, got `:` then {:?}",
7599                        self.peek()
7600                    )));
7601                }
7602                self.advance();
7603            }
7604            other => {
7605                return Err(self.err(alloc::format!(
7606                    "expected := after plpgsql assign target, got {other:?}"
7607                )));
7608            }
7609        }
7610        let value = self.parse_expr(0)?;
7611        Ok(PlPgSqlStmt::Assign { target, value })
7612    }
7613
7614    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7615    /// [ELSE body] END IF`. `IF` keyword already consumed.
7616    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7617        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7618        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7619        loop {
7620            // <expr> THEN
7621            let cond = self.parse_expr(0)?;
7622            let then_kw = self.expect_ident_like()?;
7623            if !then_kw.eq_ignore_ascii_case("then") {
7624                return Err(self.err(alloc::format!(
7625                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7626                )));
7627            }
7628            let body = self.parse_plpgsql_stmt_list_until_end()?;
7629            branches.push((cond, body));
7630            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7631            match self.peek() {
7632                Token::Ident(s) | Token::QuotedIdent(s)
7633                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7634                {
7635                    self.advance();
7636                    continue;
7637                }
7638                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7639                    self.advance();
7640                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7641                    break;
7642                }
7643                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7644                    break;
7645                }
7646                other => {
7647                    return Err(self.err(alloc::format!(
7648                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7649                    )));
7650                }
7651            }
7652        }
7653        // Expect `END IF` (the END keyword is the one we're
7654        // looking at right now).
7655        let end_kw = self.expect_ident_like()?;
7656        if !end_kw.eq_ignore_ascii_case("end") {
7657            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7658        }
7659        let if_kw = self.expect_ident_like()?;
7660        if !if_kw.eq_ignore_ascii_case("if") {
7661            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7662        }
7663        Ok(PlPgSqlStmt::If {
7664            branches,
7665            else_branch,
7666        })
7667    }
7668
7669    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7670    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7671    /// is already consumed.
7672    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7673        let lvl_ident = self.expect_ident_like()?;
7674        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7675            "notice" => RaiseLevel::Notice,
7676            "warning" => RaiseLevel::Warning,
7677            "info" => RaiseLevel::Info,
7678            "log" => RaiseLevel::Log,
7679            "debug" => RaiseLevel::Debug,
7680            "exception" => RaiseLevel::Exception,
7681            other => {
7682                return Err(self.err(alloc::format!(
7683                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7684                )));
7685            }
7686        };
7687        // Message: required for v7.12.6. PG accepts a bare
7688        // RAISE-rethrow form (no message), reserved for future
7689        // RAISE-no-args support.
7690        let Token::String(msg) = self.peek() else {
7691            return Err(self.err(alloc::format!(
7692                "expected RAISE message string, got {:?}",
7693                self.peek()
7694            )));
7695        };
7696        let message = msg.clone();
7697        self.advance();
7698        // Optional comma-separated args (PG `%` format substitution).
7699        let mut args: Vec<Expr> = Vec::new();
7700        while matches!(self.peek(), Token::Comma) {
7701            self.advance();
7702            args.push(self.parse_expr(0)?);
7703        }
7704        Ok(PlPgSqlStmt::Raise {
7705            level,
7706            message,
7707            args,
7708        })
7709    }
7710
7711    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7712    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7713    /// migrate-042). Returns `(rebuilt_select_without_into,
7714    /// var_name)` when the pattern matches; `None` for
7715    /// regular SELECTs (those go through the embedded-SQL
7716    /// path). Token-stream surgery so the rebuilt SELECT
7717    /// parses through the regular `parse_select_stmt`.
7718    #[allow(clippy::too_many_lines)]
7719    fn try_parse_plpgsql_select_into(
7720        &mut self,
7721    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7722        // Scan forward from `self.pos + 1` (past Token::Select)
7723        // for Token::Into at paren-depth 0, stopping at the
7724        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7725        // end the plpgsql statement.
7726        let start = self.pos;
7727        let mut into_pos: Option<usize> = None;
7728        let mut depth: i32 = 0;
7729        let mut i = start + 1;
7730        while i < self.tokens.len() {
7731            match &self.tokens[i] {
7732                Token::LParen => depth += 1,
7733                Token::RParen => depth -= 1,
7734                Token::Semicolon if depth == 0 => break,
7735                Token::Ident(s)
7736                    if depth == 0
7737                        && (s.eq_ignore_ascii_case("end")
7738                            || s.eq_ignore_ascii_case("else")
7739                            || s.eq_ignore_ascii_case("elsif")) =>
7740                {
7741                    break;
7742                }
7743                Token::Into if depth == 0 => {
7744                    into_pos = Some(i);
7745                    break;
7746                }
7747                _ => {}
7748            }
7749            i += 1;
7750        }
7751        let Some(into_at) = into_pos else {
7752            return Ok(None);
7753        };
7754        // The token immediately after INTO must be the target
7755        // var ident; anything else (e.g. INSERT INTO table)
7756        // ruled out by the depth-0 check above. Capture it.
7757        let var = match self.tokens.get(into_at + 1) {
7758            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7759            other => {
7760                return Err(self.err(alloc::format!(
7761                    "expected variable name after SELECT … INTO, got {other:?}"
7762                )));
7763            }
7764        };
7765        // Find the end of the plpgsql SELECT INTO statement —
7766        // same boundary rules as the depth-0 scan above.
7767        let mut end = into_at + 2;
7768        let mut depth2: i32 = 0;
7769        while end < self.tokens.len() {
7770            match &self.tokens[end] {
7771                Token::LParen => depth2 += 1,
7772                Token::RParen => depth2 -= 1,
7773                Token::Semicolon if depth2 == 0 => break,
7774                Token::Ident(s)
7775                    if depth2 == 0
7776                        && (s.eq_ignore_ascii_case("end")
7777                            || s.eq_ignore_ascii_case("else")
7778                            || s.eq_ignore_ascii_case("elsif")) =>
7779                {
7780                    break;
7781                }
7782                _ => {}
7783            }
7784            end += 1;
7785        }
7786        // Rebuild a token stream that represents the SELECT
7787        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7788        // post-var tokens up to statement end]. Run the
7789        // regular `parse_select_stmt` against it.
7790        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7791        for j in start..into_at {
7792            rebuilt.push(self.tokens[j].clone());
7793        }
7794        for j in (into_at + 2)..end {
7795            rebuilt.push(self.tokens[j].clone());
7796        }
7797        rebuilt.push(Token::Eof);
7798        let saved_pos = self.pos;
7799        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7800        self.pos = 0;
7801        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7802        if !matches!(self.peek(), Token::Select) {
7803            self.tokens = saved_tokens;
7804            self.pos = saved_pos;
7805            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7806        }
7807        let sel = self.parse_select_stmt();
7808        self.tokens = saved_tokens;
7809        self.pos = end;
7810        let sel = sel?;
7811        let Statement::Select(body) = sel else {
7812            return Err(self.err(alloc::format!(
7813                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7814            )));
7815        };
7816        Ok(Some((body, var)))
7817    }
7818
7819    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7820        // v7.16.1 — read the head token DIRECTLY rather than
7821        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7822        // strip (`public.t` → `t`) inside `expect_ident_like`
7823        // greedily consumes any `ident . ident` pair, which
7824        // silently turned every `NEW.col := …` /
7825        // `OLD.col := …` plpgsql assignment into a Local("col")
7826        // assignment — the head "new"/"old" was eaten as if it
7827        // were a schema name and the Dot was consumed too, so
7828        // this function's own `peek() == Token::Dot` check
7829        // below never fired. Every BEFORE trigger that rewrote
7830        // a NEW cell was a silent no-op for two major releases
7831        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7832        // gate failures were investigated as v7.16.1 backlog.
7833        let head = match self.advance() {
7834            Token::Ident(s) | Token::QuotedIdent(s) => s,
7835            other => {
7836                return Err(self.err(alloc::format!(
7837                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7838                )));
7839            }
7840        };
7841        if matches!(self.peek(), Token::Dot) {
7842            self.advance();
7843            let col = self.expect_ident_like()?;
7844            if head.eq_ignore_ascii_case("new") {
7845                return Ok(AssignTarget::NewColumn(col));
7846            }
7847            if head.eq_ignore_ascii_case("old") {
7848                return Ok(AssignTarget::OldColumn(col));
7849            }
7850            return Err(self.err(alloc::format!(
7851                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7852                 got {head:?}.<col>"
7853            )));
7854        }
7855        Ok(AssignTarget::Local(head))
7856    }
7857
7858    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7859        // RETURN NEW / OLD / NULL — bare-ident forms.
7860        match self.peek() {
7861            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7862                self.advance();
7863                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7864            }
7865            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7866                self.advance();
7867                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7868            }
7869            Token::Null => {
7870                self.advance();
7871                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7872            }
7873            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7874            // per PL/pgSQL convention.
7875            Token::Semicolon => {
7876                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7877            }
7878            _ => {}
7879        }
7880        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7881        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7882        // caller-visible effect (blocks don't return sets), so we
7883        // desugar it identically to PERFORM: parse the SELECT (or
7884        // EXECUTE dynamic) as embedded SQL that runs for side
7885        // effects and discards the result. RETURN NEXT <expr>
7886        // (single-row accumulator) queues with v7.40 SETOF function
7887        // infrastructure.
7888        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7889        // and keep going.
7890        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7891        {
7892            self.advance();
7893            let e = self.parse_expr(0)?;
7894            return Ok(PlPgSqlStmt::ReturnNext(e));
7895        }
7896        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7897        {
7898            self.advance();
7899            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7900            // rows go to the set, like the static form. It used to desugar to a
7901            // bare ExecuteDynamic, whose result was DISCARDED.
7902            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7903            {
7904                self.advance();
7905                let sql = self.parse_expr(0)?;
7906                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7907            }
7908            // Bare RETURN QUERY <select>. If the current token is
7909            // not already SELECT (e.g., the user wrote `RETURN QUERY
7910            // <projection> FROM ...` in a shorthand — rare but PG
7911            // accepts a bare projection here), splice one in. Same
7912            // trick as PERFORM.
7913            if !matches!(self.peek(), Token::Select) {
7914                self.tokens.insert(self.pos, Token::Select);
7915            }
7916            let select = self.parse_select_stmt()?;
7917            let Statement::Select(s) = select else {
7918                return Err(self.err(alloc::format!(
7919                    "expected SELECT body after RETURN QUERY, got {:?}",
7920                    self.peek()
7921                )));
7922            };
7923            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7924            // to an embedded side-effect SELECT whose rows were DISCARDED, which
7925            // in a SETOF function is the entire answer thrown away.
7926            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7927        }
7928        // Fall through: parse a full expression.
7929        let e = self.parse_expr(0)?;
7930        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7931    }
7932
7933    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7934        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7935        // are ident-shaped (the parser keys off case-insensitive
7936        // match — same shape used by the top-level Update / Delete
7937        // dispatchers at parse_one_statement).
7938        if matches!(self.peek(), Token::Insert) {
7939            self.advance();
7940            return Ok(TriggerEvent::Insert);
7941        }
7942        match self.peek() {
7943            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7944                self.advance();
7945                Ok(TriggerEvent::Update)
7946            }
7947            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7948                self.advance();
7949                Ok(TriggerEvent::Delete)
7950            }
7951            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7952                self.advance();
7953                Ok(TriggerEvent::Truncate)
7954            }
7955            other => Err(self.err(alloc::format!(
7956                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7957            ))),
7958        }
7959    }
7960
7961    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7962    ///   - (no clause) → implicit `FOR ALL TABLES`
7963    ///   - `FOR ALL TABLES`
7964    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7965    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7966    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
7967    ///     REJECTS the bare plural (`invalid publication object list`,
7968    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
7969    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7970    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7971        let name = self.expect_ident_or_string()?;
7972        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7973        // shape so existing publications keep parsing identically.
7974        let scope = if matches!(self.peek(), Token::For) {
7975            self.advance();
7976            if matches!(self.peek(), Token::All) {
7977                self.advance();
7978                if !matches!(self.peek(), Token::Tables) {
7979                    return Err(self.err(format!(
7980                        "expected TABLES after FOR ALL, got {:?}",
7981                        self.peek()
7982                    )));
7983                }
7984                self.advance();
7985                if matches!(self.peek(), Token::Except) {
7986                    self.advance();
7987                    let tables = self.parse_publication_table_list()?;
7988                    PublicationScope::AllTablesExcept(tables)
7989                } else {
7990                    PublicationScope::AllTables
7991                }
7992            } else if matches!(self.peek(), Token::Table) {
7993                self.advance();
7994                let tables = self.parse_publication_table_list()?;
7995                PublicationScope::ForTables(tables)
7996            } else if matches!(self.peek(), Token::Tables) {
7997                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7998                // plural (`FOR TABLES t`) is REJECTED (`invalid
7999                // publication object list`); TABLES only pairs with
8000                // `IN SCHEMA`. The old arm accepted it on an
8001                // unverifiable "PG 19 accepts both" claim.
8002                self.advance();
8003                if !matches!(self.peek(), Token::In) {
8004                    return Err(self.err(alloc::string::String::from(
8005                        "invalid publication object list",
8006                    )));
8007                }
8008                self.advance();
8009                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8010                    return Err(self.err(format!(
8011                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8012                        self.peek()
8013                    )));
8014                }
8015                self.advance();
8016                let schema = self.expect_ident_or_string()?;
8017                PublicationScope::TablesInSchema(schema)
8018            } else {
8019                return Err(self.err(format!(
8020                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8021                    self.peek()
8022                )));
8023            }
8024        } else {
8025            PublicationScope::AllTables
8026        };
8027        Ok(Statement::CreatePublication(CreatePublicationStatement {
8028            name,
8029            scope,
8030        }))
8031    }
8032
8033    /// v6.1.3 — Comma-separated identifier list for the publication
8034    /// FOR-clause. Requires at least one entry; empty list is a
8035    /// parse error (PG behaviour). Quoted idents are accepted; the
8036    /// names round-trip through `Display` as `quote_ident(name)`.
8037    ///
8038    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8039    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8040    /// pg_dump output. SPG's publication state today is per-table
8041    /// only (matching the pre-PG-15 surface); the col list + WHERE
8042    /// are parsed so dumps load through and the table name reaches
8043    /// `PublicationScope::ForTables`, but the filter is not enforced
8044    /// at publish time. Re-open when a customer dogfood gate
8045    /// requires per-row-filter or column-subset publish semantics
8046    /// (which gates on persistent slot state landing first, 21.12).
8047    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8048        let first = self.parse_publication_table_entry()?;
8049        let mut out = alloc::vec![first];
8050        while matches!(self.peek(), Token::Comma) {
8051            self.advance();
8052            out.push(self.parse_publication_table_entry()?);
8053        }
8054        Ok(out)
8055    }
8056
8057    /// One table entry inside a FOR TABLE clause:
8058    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8059    /// Returns just the table name; the column list + WHERE predicate
8060    /// are consumed and discarded per the parse-accept-discard
8061    /// commitment above.
8062    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8063        let name = self.expect_ident_like()?;
8064        // Optional column list — `(col, col, …)`.
8065        if matches!(self.peek(), Token::LParen) {
8066            self.advance();
8067            // Empty parens are a PG error too; require ≥ 1 column.
8068            let _ = self.expect_ident_like()?;
8069            while matches!(self.peek(), Token::Comma) {
8070                self.advance();
8071                let _ = self.expect_ident_like()?;
8072            }
8073            if !matches!(self.peek(), Token::RParen) {
8074                return Err(self.err(alloc::format!(
8075                    "expected ')' to close publication column list, got {:?}",
8076                    self.peek()
8077                )));
8078            }
8079            self.advance();
8080        }
8081        // Optional row filter — `WHERE (predicate)`.
8082        if matches!(self.peek(), Token::Where) {
8083            self.advance();
8084            if !matches!(self.peek(), Token::LParen) {
8085                return Err(self.err(alloc::format!(
8086                    "expected '(' after WHERE in publication row filter, got {:?}",
8087                    self.peek()
8088                )));
8089            }
8090            self.advance();
8091            let _ = self.parse_expr(0)?;
8092            if !matches!(self.peek(), Token::RParen) {
8093                return Err(self.err(alloc::format!(
8094                    "expected ')' to close publication WHERE filter, got {:?}",
8095                    self.peek()
8096                )));
8097            }
8098            self.advance();
8099        }
8100        Ok(name)
8101    }
8102
8103    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8104    ///                 CONNECTION '<conn>'
8105    ///                 PUBLICATION <pub> [, <pub> ...]`.
8106    ///
8107    /// The clause order is fixed (CONNECTION first, then
8108    /// PUBLICATION) to match PG. No WITH-options accepted in
8109    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8110    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8111        let name = self.expect_ident_or_string()?;
8112        if !matches!(self.peek(), Token::Connection) {
8113            return Err(self.err(format!(
8114                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8115                self.peek()
8116            )));
8117        }
8118        self.advance();
8119        let conn_str = self.expect_string_literal()?;
8120        if !matches!(self.peek(), Token::Publication) {
8121            return Err(self.err(format!(
8122                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8123                self.peek()
8124            )));
8125        }
8126        self.advance();
8127        // Reuse the publication FOR-list parser shape: at least one
8128        // identifier, comma-separated.
8129        let first = self.expect_ident_like()?;
8130        let mut publications = alloc::vec![first];
8131        while matches!(self.peek(), Token::Comma) {
8132            self.advance();
8133            publications.push(self.expect_ident_like()?);
8134        }
8135        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8136            name,
8137            conn_str,
8138            publications,
8139        }))
8140    }
8141
8142    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8143    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8144    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8145    /// that fit `u64`.
8146    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8147    /// qualifier is a *namespace* the app owns (`app.user_id`,
8148    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8149    /// to discard. So parse the raw segments here instead of
8150    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8151    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8152    /// a single segment and round-trip unchanged.
8153    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8154        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8155        loop {
8156            let seg = match self.advance() {
8157                Token::Ident(s) | Token::QuotedIdent(s) => s,
8158                other if unreserved_keyword_text(&other).is_some() => {
8159                    unreserved_keyword_text(&other).unwrap()
8160                }
8161                other => {
8162                    return Err(ParseError {
8163                        message: format!("expected parameter name, got {other:?}"),
8164                        token_pos: self.consumed_pos(),
8165                    });
8166                }
8167            };
8168            parts.push(seg);
8169            if matches!(self.peek(), Token::Dot) {
8170                self.advance();
8171                continue;
8172            }
8173            break;
8174        }
8175        Ok(parts.join(".").to_ascii_lowercase())
8176    }
8177
8178    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8179        Self::parse_set_value_inner(self)
8180    }
8181
8182    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8183        match self.advance() {
8184            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8185            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8186                Ok(crate::ast::SetValue::Default)
8187            }
8188            Token::Ident(s) | Token::QuotedIdent(s) => {
8189                let mut accum = s;
8190                while matches!(self.peek(), Token::Dot) {
8191                    self.advance();
8192                    let next = self.expect_ident_like()?;
8193                    accum.push('.');
8194                    accum.push_str(&next);
8195                }
8196                Ok(crate::ast::SetValue::Ident(accum))
8197            }
8198            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8199            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8200            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8201            // spellings that lex as keyword tokens, not idents:
8202            // `SET standard_conforming_strings = on` is in every
8203            // pg_dump preamble (`off` already lexes as an ident).
8204            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8205            // DEFAULT lexes as its keyword token, so the ident arm above
8206            // never saw it and the everyday reset form was a syntax error.
8207            Token::Default => Ok(crate::ast::SetValue::Default),
8208            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8209            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8210            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8211            // v7.14.0 — MySQL session/user variable RHS
8212            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8213            // Wrap as Ident so the SET handler can record it; the
8214            // engine treats `@VAR` / `@@VAR` values as opaque
8215            // strings.
8216            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8217            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8218            // is the common MySQL preamble shape. Allow a `+` or
8219            // `-` prefix on negative numerics for parity with PG
8220            // (some param defaults are negative).
8221            Token::Minus => match self.advance() {
8222                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8223                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8224                other => Err(self.err(format!(
8225                    "expected numeric after `-` in SET value, got {other:?}"
8226                ))),
8227            },
8228            other => Err(self.err(format!(
8229                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8230            ))),
8231        }
8232    }
8233
8234    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8235    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8236    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8237    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8238    /// present). Modes are comma-separated per PG; SPG also
8239    /// accepts space-separated for tolerance. READ ONLY / WRITE
8240    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8241    /// surface but not behaviorally honoured today).
8242    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8243    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8244    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8245    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8246    /// session default rather than forcing READ COMMITTED.
8247    fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8248        let mut level = IsolationLevel::default();
8249        let mut have_level = false;
8250        loop {
8251            // ISOLATION LEVEL …
8252            let saw_isolation =
8253                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8254            if saw_isolation {
8255                self.advance(); // ISOLATION
8256                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8257                    return Err(self.err(alloc::format!(
8258                        "expected LEVEL after ISOLATION, got {:?}",
8259                        self.peek()
8260                    )));
8261                }
8262                self.advance(); // LEVEL
8263                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8264                let w1 = self
8265                    .expect_ident_like()
8266                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8267                let lc = w1.to_ascii_lowercase();
8268                level = match lc.as_str() {
8269                    "serializable" => IsolationLevel::Serializable,
8270                    "repeatable" => {
8271                        // Expect READ
8272                        let w2 = self
8273                            .expect_ident_like()
8274                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8275                        if !w2.eq_ignore_ascii_case("read") {
8276                            return Err(self.err(alloc::format!(
8277                                "expected READ after REPEATABLE, got {w2:?}"
8278                            )));
8279                        }
8280                        IsolationLevel::RepeatableRead
8281                    }
8282                    "read" => {
8283                        let w2 = self
8284                            .expect_ident_like()
8285                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8286                        match w2.to_ascii_lowercase().as_str() {
8287                            "committed" => IsolationLevel::ReadCommitted,
8288                            "uncommitted" => IsolationLevel::ReadUncommitted,
8289                            other => {
8290                                return Err(self.err(alloc::format!(
8291                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8292                                )));
8293                            }
8294                        }
8295                    }
8296                    other => {
8297                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8298                    }
8299                };
8300                have_level = true;
8301            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8302                // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8303                self.advance();
8304                match self.peek().clone() {
8305                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8306                        self.advance();
8307                    }
8308                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8309                        self.advance();
8310                    }
8311                    other => {
8312                        return Err(self.err(alloc::format!(
8313                            "expected ONLY or WRITE after READ, got {other:?}"
8314                        )));
8315                    }
8316                }
8317            } else if matches!(self.peek(), Token::Not) {
8318                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8319                self.advance();
8320                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8321                    return Err(self.err(alloc::format!(
8322                        "expected DEFERRABLE after NOT, got {:?}",
8323                        self.peek()
8324                    )));
8325                }
8326                self.advance();
8327            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8328            {
8329                self.advance();
8330            } else {
8331                break;
8332            }
8333            // Optional comma between modes.
8334            if matches!(self.peek(), Token::Comma) {
8335                self.advance();
8336            }
8337        }
8338        Ok(have_level.then_some(level))
8339    }
8340
8341    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8342        // FOR is a v6.1.2-reserved keyword (Token::For). The
8343        // other two are bare idents — they've never needed lexer
8344        // support and we keep it that way.
8345        if !matches!(self.peek(), Token::For) {
8346            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8347        }
8348        self.advance();
8349        self.expect_keyword_ident("wal")?;
8350        self.expect_keyword_ident("position")?;
8351        let pos = self.expect_u64_literal()?;
8352        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8353        {
8354            self.advance();
8355            self.expect_keyword_ident("timeout")?;
8356            Some(self.expect_u64_literal()?)
8357        } else {
8358            None
8359        };
8360        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8361    }
8362
8363    /// v6.1.7 helper — consume a `Token::Integer` and check it
8364    /// fits `u64`. WAL positions and millisecond timeouts are
8365    /// non-negative.
8366    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8367        match self.advance() {
8368            Token::Integer(n) if n >= 0 => Ok(n as u64),
8369            Token::Integer(n) => Err(ParseError {
8370                message: format!("expected non-negative integer, got {n}"),
8371                token_pos: self.consumed_pos(),
8372            }),
8373            other => Err(ParseError {
8374                message: format!("expected integer literal, got {other:?}"),
8375                token_pos: self.consumed_pos(),
8376            }),
8377        }
8378    }
8379
8380    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8381    /// ROLE '<role>' (defaults to readonly). All string slots accept
8382    /// either a quoted ident or a quoted string literal.
8383    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8384    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8385    ///
8386    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8387    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8388    /// wire role) still parses — it is a different axis from the PG attributes.
8389    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8390    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8391    /// or RESET, so the plain attribute forms keep their old path.
8392    fn peeks_db_role_setting(&self) -> bool {
8393        let mut i = self.pos + 1; // past the object's name
8394        let word = |p: usize| -> Option<String> {
8395            match self.tokens.get(p) {
8396                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8397                Some(Token::In) => Some(String::from("in")),
8398                _ => None,
8399            }
8400        };
8401        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8402            i += 3; // IN DATABASE <name>
8403        }
8404        matches!(word(i).as_deref(), Some("set" | "reset"))
8405    }
8406
8407    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8408        use crate::ast::SetDbRoleSettingStatement;
8409        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8410        // identifier, so the ordinary name reader refuses it. Same trap
8411        // as TABLE / INDEX / FULL / DEFAULT before it.
8412        let name = if matches!(self.peek(), Token::All) {
8413            self.advance();
8414            String::from("all")
8415        } else {
8416            self.expect_ident_or_string()?
8417        };
8418        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8419        let all = name.eq_ignore_ascii_case("all");
8420        let (mut database, mut role) = if is_database {
8421            (Some(name), None)
8422        } else if all {
8423            (None, None)
8424        } else {
8425            (None, Some(name))
8426        };
8427        if matches!(self.peek(), Token::In) {
8428            self.advance();
8429            self.advance(); // DATABASE
8430            database = Some(self.expect_ident_or_string()?);
8431        }
8432        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8433        self.advance(); // SET | RESET
8434        if resetting && matches!(self.peek(), Token::All) {
8435            self.advance();
8436            self.consume_until_statement_boundary();
8437            return Ok(Statement::SetDbRoleSetting(Box::new(
8438                SetDbRoleSettingStatement {
8439                    database,
8440                    role,
8441                    param: None,
8442                    value: None,
8443                },
8444            )));
8445        }
8446        let param = self.expect_ident_like()?;
8447        let value = if resetting {
8448            None
8449        } else {
8450            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8451            // KEYWORD, so the ident-only check missed it and consumed
8452            // the word itself as the value — the same trap as ALL, one
8453            // clause over.
8454            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8455                self.advance();
8456            }
8457            Some(self.take_guc_value())
8458        };
8459        self.consume_until_statement_boundary();
8460        Ok(Statement::SetDbRoleSetting(Box::new(
8461            SetDbRoleSettingStatement {
8462                database,
8463                role,
8464                param: Some(param),
8465                value,
8466            },
8467        )))
8468    }
8469
8470    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8471    /// a quoted literal loses its quotes, a bare word or number does not.
8472    fn take_guc_value(&mut self) -> String {
8473        match self.advance() {
8474            Token::String(s) => s,
8475            Token::Integer(n) => format!("{n}"),
8476            Token::Float(f) => format!("{f}"),
8477            Token::Ident(s) | Token::QuotedIdent(s) => s,
8478            other => format!("{other:?}"),
8479        }
8480    }
8481
8482    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8483        let name = self.expect_ident_or_string()?;
8484        if self.peek_keyword_ident("with") {
8485            self.advance();
8486        }
8487        let mut password = String::new();
8488        let mut role = String::new();
8489        let mut login: Option<bool> = None;
8490        let mut inherit: Option<bool> = None;
8491        let mut superuser: Option<bool> = None;
8492        // Not a `while let`: the pattern would borrow `self` across the
8493        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8494        #[allow(clippy::while_let_loop)]
8495        loop {
8496            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8497                break;
8498            };
8499            match w.to_ascii_lowercase().as_str() {
8500                "password" => {
8501                    self.advance();
8502                    password = self.expect_string_literal()?;
8503                }
8504                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8505                // is the same slot.
8506                "encrypted" => {
8507                    self.advance();
8508                    self.expect_keyword_ident("password")?;
8509                    password = self.expect_string_literal()?;
8510                }
8511                "login" => {
8512                    self.advance();
8513                    login = Some(true);
8514                }
8515                "nologin" => {
8516                    self.advance();
8517                    login = Some(false);
8518                }
8519                "inherit" => {
8520                    self.advance();
8521                    inherit = Some(true);
8522                }
8523                "noinherit" => {
8524                    self.advance();
8525                    inherit = Some(false);
8526                }
8527                "superuser" => {
8528                    self.advance();
8529                    superuser = Some(true);
8530                }
8531                "nosuperuser" => {
8532                    self.advance();
8533                    superuser = Some(false);
8534                }
8535                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8536                "role" => {
8537                    self.advance();
8538                    role = self.expect_string_literal()?;
8539                }
8540                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8541                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8542                // accepted and ignored so a pg_dump role block restores. They
8543                // gate capabilities SPG does not have.
8544                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8545                | "noreplication" | "bypassrls" | "nobypassrls" => {
8546                    self.advance();
8547                }
8548                "connection" => {
8549                    self.advance();
8550                    self.expect_keyword_ident("limit")?;
8551                    self.advance(); // the number
8552                }
8553                "valid" => {
8554                    self.advance();
8555                    self.expect_keyword_ident("until")?;
8556                    self.expect_string_literal()?;
8557                }
8558                _ => break,
8559            }
8560        }
8561        if role.is_empty() {
8562            role = "readonly".to_string();
8563        }
8564        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8565            name,
8566            password,
8567            role,
8568            login,
8569            inherit,
8570            superuser,
8571            is_user,
8572        }))
8573    }
8574
8575    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8576    /// consumed the USING / WITH CHECK keyword.
8577    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8578        if !matches!(self.peek(), Token::LParen) {
8579            return Err(self.err(alloc::format!(
8580                "expected '(' after {clause}, got {:?}",
8581                self.peek()
8582            )));
8583        }
8584        self.advance();
8585        let e = self.parse_expr(0)?;
8586        if !matches!(self.peek(), Token::RParen) {
8587            return Err(self.err(alloc::format!(
8588                "expected ')' to close {clause}, got {:?}",
8589                self.peek()
8590            )));
8591        }
8592        self.advance();
8593        Ok(e)
8594    }
8595
8596    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8597    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8598        let mut roles = Vec::new();
8599        loop {
8600            roles.push(self.expect_ident_like()?);
8601            if matches!(self.peek(), Token::Comma) {
8602                self.advance();
8603            } else {
8604                break;
8605            }
8606        }
8607        Ok(roles)
8608    }
8609
8610    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8611    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8612    /// `CREATE POLICY`.
8613    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8614        use crate::ast::PolicyCmd;
8615        let name = self.expect_ident_like()?;
8616        if !matches!(self.peek(), Token::On) {
8617            return Err(self.err(alloc::format!(
8618                "expected ON after CREATE POLICY name, got {:?}",
8619                self.peek()
8620            )));
8621        }
8622        self.advance();
8623        let table = self.expect_ident_like()?;
8624
8625        let mut permissive = true;
8626        if matches!(self.peek(), Token::As) {
8627            self.advance();
8628            let w = self.expect_ident_like()?;
8629            permissive = if w.eq_ignore_ascii_case("permissive") {
8630                true
8631            } else if w.eq_ignore_ascii_case("restrictive") {
8632                false
8633            } else {
8634                return Err(self.err(alloc::format!(
8635                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8636                )));
8637            };
8638        }
8639
8640        let mut cmd = PolicyCmd::All;
8641        if matches!(self.peek(), Token::For) {
8642            self.advance();
8643            cmd = self.parse_policy_cmd()?;
8644        }
8645
8646        let mut roles = Vec::new();
8647        if matches!(self.peek(), Token::To) {
8648            self.advance();
8649            roles = self.parse_policy_roles()?;
8650        }
8651
8652        let mut using = None;
8653        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8654        {
8655            self.advance();
8656            using = Some(self.parse_paren_expr("USING")?);
8657        }
8658
8659        let mut with_check = None;
8660        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8661        {
8662            self.advance();
8663            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8664            {
8665                return Err(self.err(alloc::format!(
8666                    "expected CHECK after WITH, got {:?}",
8667                    self.peek()
8668                )));
8669            }
8670            self.advance();
8671            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8672        }
8673
8674        // Clause-per-command matrix (PG wording).
8675        match cmd {
8676            PolicyCmd::Insert => {
8677                if using.is_some() {
8678                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8679                }
8680            }
8681            PolicyCmd::Select | PolicyCmd::Delete => {
8682                if with_check.is_some() {
8683                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8684                }
8685            }
8686            PolicyCmd::Update | PolicyCmd::All => {}
8687        }
8688
8689        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8690            name,
8691            table,
8692            permissive,
8693            cmd,
8694            roles,
8695            using,
8696            with_check,
8697        }))
8698    }
8699
8700    /// v7.39 (RLS) — the command word after `FOR`.
8701    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8702        use crate::ast::PolicyCmd;
8703        match self.peek().clone() {
8704            Token::All => {
8705                self.advance();
8706                Ok(PolicyCmd::All)
8707            }
8708            Token::Select => {
8709                self.advance();
8710                Ok(PolicyCmd::Select)
8711            }
8712            Token::Insert => {
8713                self.advance();
8714                Ok(PolicyCmd::Insert)
8715            }
8716            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8717                self.advance();
8718                Ok(PolicyCmd::Update)
8719            }
8720            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8721                self.advance();
8722                Ok(PolicyCmd::Delete)
8723            }
8724            other => Err(self.err(alloc::format!(
8725                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8726            ))),
8727        }
8728    }
8729
8730    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8731    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8732    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8733        let name = self.expect_ident_like()?;
8734        if !matches!(self.peek(), Token::On) {
8735            return Err(self.err(alloc::format!(
8736                "expected ON after ALTER POLICY name, got {:?}",
8737                self.peek()
8738            )));
8739        }
8740        self.advance();
8741        let table = self.expect_ident_like()?;
8742
8743        // RENAME TO new
8744        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8745        {
8746            self.advance();
8747            if !matches!(self.peek(), Token::To) {
8748                return Err(self.err(alloc::format!(
8749                    "expected TO after RENAME, got {:?}",
8750                    self.peek()
8751                )));
8752            }
8753            self.advance();
8754            let new = self.expect_ident_like()?;
8755            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8756                name,
8757                table,
8758                rename_to: Some(new),
8759                roles: None,
8760                using: None,
8761                with_check: None,
8762            }));
8763        }
8764
8765        let mut roles = None;
8766        if matches!(self.peek(), Token::To) {
8767            self.advance();
8768            roles = Some(self.parse_policy_roles()?);
8769        }
8770        let mut using = None;
8771        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8772        {
8773            self.advance();
8774            using = Some(self.parse_paren_expr("USING")?);
8775        }
8776        let mut with_check = None;
8777        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8778        {
8779            self.advance();
8780            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8781            {
8782                return Err(self.err(alloc::format!(
8783                    "expected CHECK after WITH, got {:?}",
8784                    self.peek()
8785                )));
8786            }
8787            self.advance();
8788            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8789        }
8790        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8791            name,
8792            table,
8793            rename_to: None,
8794            roles,
8795            using,
8796            with_check,
8797        }))
8798    }
8799
8800    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8801    /// `DROP POLICY`.
8802    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8803        let if_exists = self.consume_if_exists();
8804        let name = self.expect_ident_like()?;
8805        if !matches!(self.peek(), Token::On) {
8806            return Err(self.err(alloc::format!(
8807                "expected ON after DROP POLICY name, got {:?}",
8808                self.peek()
8809            )));
8810        }
8811        self.advance();
8812        let table = self.expect_ident_like()?;
8813        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8814            name,
8815            table,
8816            if_exists,
8817        }))
8818    }
8819}
8820fn wrap_from_leaves(
8821    e: &mut Expr,
8822    names: &[String],
8823    make: &dyn Fn(Expr) -> Expr,
8824    refs: &dyn Fn(&Expr) -> bool,
8825) {
8826    if let Expr::Column(c) = e {
8827        if c.qualifier
8828            .as_deref()
8829            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8830        {
8831            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8832            *e = make(taken);
8833        }
8834        return;
8835    }
8836    match e {
8837        Expr::Binary { lhs, rhs, .. } => {
8838            wrap_from_leaves(lhs, names, make, refs);
8839            wrap_from_leaves(rhs, names, make, refs);
8840        }
8841        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8842            wrap_from_leaves(expr, names, make, refs)
8843        }
8844        Expr::FunctionCall { args, .. } => {
8845            for a in args.iter_mut() {
8846                wrap_from_leaves(a, names, make, refs);
8847            }
8848        }
8849        Expr::Case {
8850            operand,
8851            branches,
8852            else_branch,
8853        } => {
8854            if let Some(o) = operand.as_deref_mut() {
8855                wrap_from_leaves(o, names, make, refs);
8856            }
8857            for (w, t) in branches.iter_mut() {
8858                wrap_from_leaves(w, names, make, refs);
8859                wrap_from_leaves(t, names, make, refs);
8860            }
8861            if let Some(el) = else_branch.as_deref_mut() {
8862                wrap_from_leaves(el, names, make, refs);
8863            }
8864        }
8865        // Compound variants the walk doesn't decompose: keep the
8866        // pre-D.30 behavior — wrap the whole sub-expr if it touches
8867        // a source table, so nothing regresses.
8868        other => {
8869            if refs(other) {
8870                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8871                *other = make(taken);
8872            }
8873        }
8874    }
8875}
8876
8877/// v7.39 (round 241) — does this expression reference any of the FROM /
8878/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8879/// lowerings)?
8880fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8881    match e {
8882        Expr::Column(c) => c
8883            .qualifier
8884            .as_deref()
8885            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8886        Expr::Binary { lhs, rhs, .. } => {
8887            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8888        }
8889        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8890        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8891        Expr::Case {
8892            operand,
8893            branches,
8894            else_branch,
8895        } => {
8896            operand
8897                .as_deref()
8898                .is_some_and(|o| expr_refs_tables(o, names))
8899                || branches
8900                    .iter()
8901                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8902                || else_branch
8903                    .as_deref()
8904                    .is_some_and(|el| expr_refs_tables(el, names))
8905        }
8906        _ => false,
8907    }
8908}
8909
8910impl Parser {
8911    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8912    /// Caller already consumed the leading `UPDATE` ident.
8913    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8914    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8915    /// after the target name has been read. `JOIN` is a reserved token;
8916    /// the qualifiers are bare idents.
8917    fn peek_is_update_join_start(&self) -> bool {
8918        match self.peek() {
8919            // JOIN and its qualifiers are reserved lexer tokens (the grammar
8920            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8921            Token::Join
8922            | Token::Inner
8923            | Token::Left
8924            | Token::Right
8925            | Token::Cross
8926            | Token::Full => true,
8927            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8928            Token::Ident(s) | Token::QuotedIdent(s) => {
8929                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8930            }
8931            _ => false,
8932        }
8933    }
8934
8935    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8936    /// USER-variable assignment. Its own per-session namespace, an arbitrary
8937    /// expression on the right, and `:=` as a second spelling of `=`.
8938    ///
8939    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8940    /// from sits on the nesting recursion chain (a CTE body, a subquery),
8941    /// and holding this loop's `Vec` + `String` locals there overflowed the
8942    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8943    #[inline(never)]
8944    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8945        let mut assigns: Vec<(String, Expr)> = Vec::new();
8946        let mut settings: Vec<(String, Expr)> = Vec::new();
8947        loop {
8948            // v7.39 (round 554) — a plain NAME here is a session
8949            // setting, not a user variable. mysqldump writes the two in
8950            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8951            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8952            // changes it — and this refused the mixture outright, so no
8953            // dump could be restored past its preamble.
8954            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8955                self.advance();
8956                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8957                    return Err(self.err(alloc::format!(
8958                        "expected `=` after {name}, got {:?}",
8959                        self.peek()
8960                    )));
8961                }
8962                self.advance();
8963                let value = self.parse_expr(0)?;
8964                settings.push((name.to_ascii_lowercase(), value));
8965                if matches!(self.peek(), Token::Comma) {
8966                    self.advance();
8967                    continue;
8968                }
8969                break;
8970            }
8971            let Token::SessionVar(raw) = self.peek().clone() else {
8972                return Err(self.err(alloc::format!(
8973                    "expected a user variable after SET, got {:?}",
8974                    self.peek()
8975                )));
8976            };
8977            if raw.starts_with("@@") {
8978                return Err(self.err(alloc::string::String::from(
8979                    "cannot mix `@@` settings with `@` user variables in one SET",
8980                )));
8981            }
8982            self.advance();
8983            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8984                return Err(self.err(alloc::format!(
8985                    "expected `=` or `:=` after {raw}, got {:?}",
8986                    self.peek()
8987                )));
8988            }
8989            self.advance();
8990            let value = self.parse_expr(0)?;
8991            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8992            if matches!(self.peek(), Token::Comma) {
8993                self.advance();
8994                continue;
8995            }
8996            break;
8997        }
8998        Ok(Statement::SetUserVars(assigns, settings))
8999    }
9000
9001    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9002        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9003        // NAMED `only` until now, which failed on `relation "only" does
9004        // not exist`. The lookahead is what keeps a table actually
9005        // called `only` working: the keyword is only a keyword when a
9006        // TABLE NAME follows it — and `SET` arrives as an identifier
9007        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9008        // for the table and die on the `=`. Measured by the pin.
9009        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9010            if s.eq_ignore_ascii_case("only"))
9011            && matches!(
9012                self.tokens.get(self.pos + 1),
9013                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9014            );
9015        if only {
9016            self.advance();
9017        }
9018        let table = self.expect_ident_like()?;
9019        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9020        // bare spelling; a bare identifier that is the SET keyword itself
9021        // is the clause, not an alias.
9022        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9023        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9024        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9025        // following JOIN a syntax error.
9026        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9027        let alias = if matches!(self.peek(), Token::As) {
9028            self.advance();
9029            Some(self.expect_ident_like()?)
9030        } else {
9031            match self.peek() {
9032                Token::Ident(s) | Token::QuotedIdent(s)
9033                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9034                {
9035                    let a = s.clone();
9036                    self.advance();
9037                    Some(a)
9038                }
9039                _ => None,
9040            }
9041        };
9042        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9043        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9044        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9045        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9046        // The FIRST table is the mutation target and the rest are sources —
9047        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9048        // SPG already lowers onto correlated subqueries. So rewind, let
9049        // `parse_from_clause` read the whole list (it handles aliases, comma
9050        // lists, and every JOIN form), then peel the target off the front.
9051        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9052            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9053        {
9054            // NOTE: `advance()` destroys the tokens it returns
9055            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9056            // is NOT possible — the tail is read forward, once, through the
9057            // same grammar `parse_from_clause` uses after its primary.
9058            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9059            let mut joins = self.parse_from_joins(&target_qual)?;
9060            if joins.is_empty() {
9061                return Err(self.err(alloc::string::String::from(
9062                    "multi-table UPDATE needs at least one source table",
9063                )));
9064            }
9065            let head = joins.remove(0);
9066            // A LEFT join keeps every target row (the unmatched ones see NULL
9067            // on the source side), so it must NOT get the EXISTS row filter
9068            // the inner / comma forms use.
9069            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9070            let src = FromClause {
9071                primary: head.table,
9072                joins,
9073            };
9074            (Some(src), head.on, outer)
9075        } else {
9076            (None, None, false)
9077        };
9078        self.expect_keyword_ident("set")?;
9079        let mut assignments = Vec::new();
9080        loop {
9081            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9082            // …)` — the parenthesized multi-assignment. Expressions
9083            // assign positionally; a subquery RHS clones per column
9084            // keeping only the Nth projection item.
9085            if matches!(self.peek(), Token::LParen) {
9086                self.advance();
9087                let mut cols = alloc::vec![self.expect_ident_like()?];
9088                while matches!(self.peek(), Token::Comma) {
9089                    self.advance();
9090                    cols.push(self.expect_ident_like()?);
9091                }
9092                if !matches!(self.peek(), Token::RParen) {
9093                    return Err(self.err(format!(
9094                        "expected ')' after SET column list, got {:?}",
9095                        self.peek()
9096                    )));
9097                }
9098                self.advance();
9099                if !matches!(self.peek(), Token::Eq) {
9100                    return Err(self.err(format!(
9101                        "expected `=` after SET column list, got {:?}",
9102                        self.peek()
9103                    )));
9104                }
9105                self.advance();
9106                if !matches!(self.peek(), Token::LParen) {
9107                    return Err(self.err(format!(
9108                        "expected '(' after SET (…) =, got {:?}",
9109                        self.peek()
9110                    )));
9111                }
9112                self.advance();
9113                if matches!(self.peek(), Token::Select) {
9114                    let inner = match self.parse_select_stmt()? {
9115                        Statement::Select(s) => s,
9116                        other => {
9117                            return Err(self.err(alloc::format!(
9118                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9119                            )));
9120                        }
9121                    };
9122                    if !matches!(self.peek(), Token::RParen) {
9123                        return Err(self.err(format!(
9124                            "expected ')' after SET subquery, got {:?}",
9125                            self.peek()
9126                        )));
9127                    }
9128                    self.advance();
9129                    if inner.items.len() != cols.len() {
9130                        return Err(self.err(alloc::format!(
9131                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9132                            cols.len(),
9133                            inner.items.len()
9134                        )));
9135                    }
9136                    for (i, col) in cols.into_iter().enumerate() {
9137                        let mut sub = inner.clone();
9138                        sub.items = alloc::vec![sub.items[i].clone()];
9139                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9140                    }
9141                } else {
9142                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9143                    while matches!(self.peek(), Token::Comma) {
9144                        self.advance();
9145                        exprs.push(self.parse_expr(0)?);
9146                    }
9147                    if !matches!(self.peek(), Token::RParen) {
9148                        return Err(self.err(format!(
9149                            "expected ')' after SET row values, got {:?}",
9150                            self.peek()
9151                        )));
9152                    }
9153                    self.advance();
9154                    if exprs.len() != cols.len() {
9155                        return Err(self.err(alloc::format!(
9156                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9157                            cols.len(),
9158                            exprs.len()
9159                        )));
9160                    }
9161                    for (col, e) in cols.into_iter().zip(exprs) {
9162                        assignments.push((col, e));
9163                    }
9164                }
9165                if matches!(self.peek(), Token::Comma) {
9166                    self.advance();
9167                    continue;
9168                }
9169                break;
9170            }
9171            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9172            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9173            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9174            // `public.` dump qualifiers), so the qualifier has to be read off
9175            // the token stream first — otherwise `SET b.v = 888` would write
9176            // to the TARGET table's `v` while naming a source table, a
9177            // silent-wrong. A qualifier naming a SOURCE table means a
9178            // multi-TARGET update — mutating two tables in one statement —
9179            // which SPG does not model, so it is refused loudly.
9180            let set_qual: Option<String> = if mysql_from.is_some()
9181                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9182            {
9183                match self.peek() {
9184                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9185                    _ => None,
9186                }
9187            } else {
9188                None
9189            };
9190            let col = self.expect_ident_like()?;
9191            if let Some(q) = set_qual {
9192                let names_target = q.eq_ignore_ascii_case(&table)
9193                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9194                if !names_target {
9195                    return Err(self.err(alloc::format!(
9196                        "multi-table UPDATE can only assign to its first table \
9197                         ({table}); `{q}.{col}` targets another table"
9198                    )));
9199                }
9200            }
9201            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9202            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9203            // `__column_default` marker lowering just below). PG assigns to the
9204            // i-th (1-based) element, NULL-padding when i exceeds the length.
9205            if matches!(self.peek(), Token::LBracket) {
9206                self.advance();
9207                let index = self.parse_expr(0)?;
9208                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9209                // (and the open `arr[lo:]`), lowered to
9210                // `__array_assign_slice`. Only the single-subscript form
9211                // parsed before, so a slice assignment was a syntax error.
9212                let mut slice_hi: Option<Option<Expr>> = None;
9213                if matches!(self.peek(), Token::Colon) {
9214                    self.advance();
9215                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9216                        None
9217                    } else {
9218                        Some(self.parse_expr(0)?)
9219                    });
9220                }
9221                if !matches!(self.peek(), Token::RBracket) {
9222                    return Err(self.err(format!(
9223                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9224                        self.peek()
9225                    )));
9226                }
9227                self.advance();
9228                if !matches!(self.peek(), Token::Eq) {
9229                    return Err(self.err(format!(
9230                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9231                        self.peek()
9232                    )));
9233                }
9234                self.advance();
9235                let value = self.parse_expr(0)?;
9236                // PG merges several subscript writes to the same column into one
9237                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9238                // assignment to `col` rather than each overwriting the original.
9239                let existing = assignments.iter().position(|(c, _)| c == &col);
9240                let base = match existing {
9241                    Some(i) => assignments[i].1.clone(),
9242                    None => Expr::Column(ColumnName {
9243                        qualifier: None,
9244                        name: col.clone(),
9245                    }),
9246                };
9247                let assigned = match slice_hi {
9248                    None => Expr::FunctionCall {
9249                        name: "__array_assign".to_string(),
9250                        args: alloc::vec![base, index, value],
9251                    },
9252                    Some(hi) => Expr::FunctionCall {
9253                        name: "__array_assign_slice".to_string(),
9254                        args: alloc::vec![
9255                            base,
9256                            index,
9257                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9258                            value,
9259                        ],
9260                    },
9261                };
9262                match existing {
9263                    Some(i) => assignments[i].1 = assigned,
9264                    None => assignments.push((col, assigned)),
9265                }
9266                if matches!(self.peek(), Token::Comma) {
9267                    self.advance();
9268                    continue;
9269                }
9270                break;
9271            }
9272            if !matches!(self.peek(), Token::Eq) {
9273                return Err(self.err(format!(
9274                    "expected `=` after column name in UPDATE SET, got {:?}",
9275                    self.peek()
9276                )));
9277            }
9278            self.advance();
9279            // `SET col = DEFAULT` — the column's declared default;
9280            // rides out as a marker call the update executor
9281            // resolves against the schema.
9282            let value = if matches!(self.peek(), Token::Default) {
9283                self.advance();
9284                Expr::FunctionCall {
9285                    name: "__column_default".to_string(),
9286                    args: Vec::new(),
9287                }
9288            } else {
9289                self.parse_expr(0)?
9290            };
9291            assignments.push((col, value));
9292            if matches!(self.peek(), Token::Comma) {
9293                self.advance();
9294                continue;
9295            }
9296            break;
9297        }
9298        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9299        // update. Lowers onto the correlated-subquery machinery:
9300        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9301        // and each assignment that references a FROM-list table
9302        // wraps into a correlated scalar subquery
9303        // (SELECT expr FROM src WHERE cond). Equivalent for the
9304        // unique-join shape (the overwhelmingly common one); a
9305        // multi-match, which PG resolves by arbitrary pick,
9306        // surfaces as a scalar-subquery cardinality error instead
9307        // of a silent arbitrary result.
9308        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9309        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9310        // the SAME lowering below. Both spellings together is not legal in
9311        // either dialect.
9312        let from_clause = if let Some(fc) = mysql_from {
9313            if matches!(self.peek(), Token::From) {
9314                return Err(self.err(alloc::string::String::from(
9315                    "multi-table UPDATE already names its sources; drop the FROM clause",
9316                )));
9317            }
9318            Some(fc)
9319        } else if matches!(self.peek(), Token::From) {
9320            self.advance();
9321            Some(self.parse_from_clause()?)
9322        } else {
9323            None
9324        };
9325        let where_ = if matches!(self.peek(), Token::Where) {
9326            self.advance();
9327            Some(self.parse_expr(0)?)
9328        } else {
9329            None
9330        };
9331        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9332        // and the TARGET-row filter are NOT the same predicate once a LEFT
9333        // join is involved:
9334        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9335        //     one conjunction, and the whole thing filters target rows via
9336        //     EXISTS.
9337        //   * LEFT join: only the ON predicate belongs inside the source
9338        //     subquery. The WHERE still filters TARGET rows (with source
9339        //     columns read through the correlated subquery, which yields NULL
9340        //     for an unmatched row — exactly LEFT-join semantics).
9341        // Round 420 folded ON into WHERE unconditionally and then dropped the
9342        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9343        // WHERE a.id > 1` updated EVERY row.
9344        let sub_where = match (mysql_on.clone(), where_.clone()) {
9345            _ if mysql_outer => mysql_on.clone(),
9346            (Some(on), Some(w)) => Some(Expr::Binary {
9347                lhs: Box::new(on),
9348                op: crate::ast::BinOp::And,
9349                rhs: Box::new(w),
9350            }),
9351            (Some(on), None) => Some(on),
9352            (None, w) => w,
9353        };
9354        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9355        // has no such clause on UPDATE, so this is accepted only under the
9356        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9357        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9358        let mut returning = self.parse_optional_returning()?;
9359        // v7.39 (round 533) — kept for the engine, which can resolve the
9360        // UNQUALIFIED leaves this lowering has to leave alone.
9361        let from_sources = from_clause.as_ref().map(|fc| {
9362            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9363                from: fc.clone(),
9364                sub_where: sub_where.clone(),
9365            })
9366        });
9367        let (assignments, where_) = if let Some(fc) = from_clause {
9368            let names: Vec<String> = core::iter::once(&fc.primary)
9369                .chain(fc.joins.iter().map(|j| &j.table))
9370                .flat_map(|t| {
9371                    t.alias
9372                        .clone()
9373                        .into_iter()
9374                        .chain(core::iter::once(t.name.clone()))
9375                })
9376                .collect();
9377            let refs_list = |e: &Expr| -> bool {
9378                fn walk(e: &Expr, names: &[String]) -> bool {
9379                    match e {
9380                        Expr::Column(c) => c
9381                            .qualifier
9382                            .as_deref()
9383                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9384                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9385                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9386                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9387                        Expr::Case {
9388                            operand,
9389                            branches,
9390                            else_branch,
9391                        } => {
9392                            operand.as_deref().is_some_and(|o| walk(o, names))
9393                                || branches
9394                                    .iter()
9395                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9396                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9397                        }
9398                        _ => false,
9399                    }
9400                }
9401                walk(e, &names)
9402            };
9403            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9404                locking: None,
9405                ctes: Vec::new(),
9406                distinct: false,
9407                distinct_on: Vec::new(),
9408                items,
9409                from: Some(fc.clone()),
9410                where_: sub_where.clone(),
9411                group_by: None,
9412                group_by_all: false,
9413                having: None,
9414                unions: Vec::new(),
9415                order_by: Vec::new(),
9416                limit: None,
9417                offset: None,
9418                limit_with_ties: false,
9419                window_check_exprs: Vec::new(),
9420            };
9421            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9422            // assignment RHS with a correlated scalar subquery, instead of
9423            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9424            // column reference (`SET v = v + u.bonus`, where `v` is the target
9425            // table's column) inside a subquery whose FROM only has the source
9426            // table, so the unqualified `v` resolved against the source and
9427            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9428            // context — where they belong — fixes it; only the source columns
9429            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9430            // compound variants the leaf-walk doesn't decompose.
9431            let make_subq = |inner: Expr| {
9432                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9433                    expr: inner,
9434                    alias: None,
9435                }])))
9436            };
9437            let assignments = assignments
9438                .into_iter()
9439                .map(|(col, mut expr)| {
9440                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9441                    (col, expr)
9442                })
9443                .collect();
9444            let exists = Expr::Exists {
9445                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9446                    expr: Expr::Literal(Literal::Integer(1)),
9447                    alias: None,
9448                }])),
9449                negated: false,
9450            };
9451            // v7.39 (round 241) — RETURNING may reference the FROM-list
9452            // tables too (`RETURNING emp.id, dept.name`); the same
9453            // leaf-to-correlated-subquery lowering the assignments get.
9454            // Without it the qualifier died at eval with "unknown table
9455            // qualifier". (RETURNING was parsed before this block — the
9456            // lowering is a pure AST transformation.)
9457            if let Some(items) = returning.as_mut() {
9458                for item in items.iter_mut() {
9459                    if let SelectItem::Expr { expr, .. } = item {
9460                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9461                    }
9462                }
9463            }
9464            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9465            // EVERY matching target row: it gets no EXISTS filter, but the
9466            // caller's WHERE still applies, with source columns read through
9467            // the correlated subquery (NULL when unmatched — LEFT-join
9468            // semantics). `sub_where` above already excluded the WHERE from
9469            // the source subquery for this case.
9470            if mysql_outer {
9471                let mut outer = where_;
9472                if let Some(w) = outer.as_mut() {
9473                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9474                }
9475                (assignments, outer)
9476            } else {
9477                (assignments, Some(exists))
9478            }
9479        } else {
9480            (assignments, where_)
9481        };
9482        Ok(Statement::Update(crate::ast::UpdateStatement {
9483            ctes: Vec::new(),
9484            table,
9485            only,
9486            alias,
9487            assignments,
9488            from_sources,
9489            where_,
9490            order_limit: update_order_limit,
9491            returning,
9492        }))
9493    }
9494
9495    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9496    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9497    /// clause and its meaning are identical, so both call this rather than
9498    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9499    /// legal. PG has no such clause on either statement, so it is read only
9500    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9501    /// errors.
9502    ///
9503    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9504    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9505    /// stack in round 430.
9506    #[inline(never)]
9507    fn parse_mysql_dml_order_limit(
9508        &mut self,
9509        what: &str,
9510    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9511        if !self.mysql_dialect {
9512            return Ok(None);
9513        }
9514        let order_by = self.parse_order_by_keys()?;
9515        let limit = if matches!(self.peek(), Token::Limit) {
9516            self.advance();
9517            let tok = self.advance();
9518            let Token::Integer(n) = tok else {
9519                return Err(self.err(alloc::format!(
9520                    "expected integer after {what} LIMIT, got {tok:?}"
9521                )));
9522            };
9523            // MySQL rejects the `LIMIT offset, count` form here — only a
9524            // single row count is legal on a DML statement.
9525            if matches!(self.peek(), Token::Comma) {
9526                return Err(self.err(alloc::format!(
9527                    "{what} LIMIT takes a row count, not an offset"
9528                )));
9529            }
9530            let n = u32::try_from(n)
9531                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9532            Some(n)
9533        } else {
9534            None
9535        };
9536        if order_by.is_empty() && limit.is_none() {
9537            return Ok(None);
9538        }
9539        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9540            order_by,
9541            limit,
9542        })))
9543    }
9544
9545    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9546    /// the leading `DELETE` ident.
9547    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9548        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9549        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9550        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9551        // parse here; it reaches the existing USING path with the target
9552        // repeated in the list, which the source-list peel below handles.)
9553        // More than one name is a multi-TARGET delete, which SPG does not
9554        // model; it is refused rather than half-applied.
9555        let mysql_pre_target: Option<String> =
9556            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9557                let first = self.expect_ident_like()?;
9558                if matches!(self.peek(), Token::Comma) {
9559                    return Err(self.err(alloc::format!(
9560                        "multi-table DELETE can only delete from one table; \
9561                     `DELETE {first}, …` names several"
9562                    )));
9563                }
9564                Some(first)
9565            } else {
9566                None
9567            };
9568        if !matches!(self.peek(), Token::From) {
9569            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9570        }
9571        self.advance();
9572        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9573        // lookahead as the UPDATE spelling.
9574        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9575            if s.eq_ignore_ascii_case("only"))
9576            && matches!(
9577                self.tokens.get(self.pos + 1),
9578                Some(Token::Ident(_) | Token::QuotedIdent(_))
9579            );
9580        if only {
9581            self.advance();
9582        }
9583        let table = self.expect_ident_like()?;
9584        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9585        // spelling must not swallow the clause keywords that can follow
9586        // the target.
9587        let alias = if matches!(self.peek(), Token::As) {
9588            self.advance();
9589            Some(self.expect_ident_like()?)
9590        } else {
9591            match self.peek() {
9592                Token::Ident(s) | Token::QuotedIdent(s)
9593                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9594                {
9595                    let a = s.clone();
9596                    self.advance();
9597                    Some(a)
9598                }
9599                _ => None,
9600            }
9601        };
9602        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9603        // through the SAME join grammar the FROM clause uses (see the
9604        // `advance()`-destroys-tokens note on `parse_from_joins`).
9605        let mut mysql_on: Option<Expr> = None;
9606        let mut mysql_outer = false;
9607        let mysql_using = if mysql_pre_target.is_some()
9608            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9609        {
9610            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9611            let mut joins = self.parse_from_joins(&target_qual)?;
9612            if joins.is_empty() {
9613                return Err(self.err(alloc::string::String::from(
9614                    "multi-table DELETE needs at least one source table",
9615                )));
9616            }
9617            let head = joins.remove(0);
9618            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9619            mysql_on = head.on;
9620            Some(FromClause {
9621                primary: head.table,
9622                joins,
9623            })
9624        } else {
9625            None
9626        };
9627        // The pre-FROM target must be the table the FROM names (or its
9628        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9629        // is not the scan target.
9630        if let Some(t) = &mysql_pre_target {
9631            let names_target = t.eq_ignore_ascii_case(&table)
9632                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9633            if !names_target {
9634                return Err(self.err(alloc::format!(
9635                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9636                )));
9637            }
9638        }
9639        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9640        // delete. Same lowering as UPDATE … FROM: the WHERE
9641        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9642        // target row by the correlated machinery.
9643        let using_clause = if let Some(fc) = mysql_using {
9644            Some(fc)
9645        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9646            self.advance();
9647            let mut fc = self.parse_from_clause()?;
9648            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9649            // repeats the TARGET as the first USING entry (PG's spelling
9650            // lists only the extra sources). Peel it so the source subquery
9651            // does not re-scan — and shadow — the target table.
9652            let primary_is_target =
9653                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9654            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9655                let head = fc.joins.remove(0);
9656                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9657                mysql_on = head.on;
9658                fc = FromClause {
9659                    primary: head.table,
9660                    joins: fc.joins,
9661                };
9662            }
9663            Some(fc)
9664        } else {
9665            None
9666        };
9667        let where_ = if matches!(self.peek(), Token::Where) {
9668            self.advance();
9669            Some(self.parse_expr(0)?)
9670        } else {
9671            None
9672        };
9673        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9674        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9675        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9676        let mut returning = self.parse_optional_returning()?;
9677        let where_ = if let Some(fc) = using_clause {
9678            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9679            // a USING-table reference in RETURNING becomes a correlated
9680            // scalar subquery over the USING list.
9681            let names: Vec<String> = core::iter::once(&fc.primary)
9682                .chain(fc.joins.iter().map(|j| &j.table))
9683                .flat_map(|t| {
9684                    t.alias
9685                        .clone()
9686                        .into_iter()
9687                        .chain(core::iter::once(t.name.clone()))
9688                })
9689                .collect();
9690            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9691            // join filters the SOURCE subquery on the ON predicate alone and
9692            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9693            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9694            // rows); every other form folds ON and WHERE into one EXISTS.
9695            let sub_where = match (mysql_on.clone(), where_.clone()) {
9696                _ if mysql_outer => mysql_on.clone(),
9697                (Some(on), Some(w)) => Some(Expr::Binary {
9698                    lhs: Box::new(on),
9699                    op: crate::ast::BinOp::And,
9700                    rhs: Box::new(w),
9701                }),
9702                (Some(on), None) => Some(on),
9703                (None, w) => w,
9704            };
9705            let exists_where = sub_where.clone();
9706            let sub_fc = fc.clone();
9707            let make_subq = move |leaf: Expr| -> Expr {
9708                Expr::ScalarSubquery(Box::new(SelectStatement {
9709                    locking: None,
9710                    ctes: Vec::new(),
9711                    distinct: false,
9712                    distinct_on: Vec::new(),
9713                    items: alloc::vec![SelectItem::Expr {
9714                        expr: leaf,
9715                        alias: None,
9716                    }],
9717                    from: Some(sub_fc.clone()),
9718                    where_: sub_where.clone(),
9719                    group_by: None,
9720                    group_by_all: false,
9721                    having: None,
9722                    unions: Vec::new(),
9723                    order_by: Vec::new(),
9724                    limit: None,
9725                    offset: None,
9726                    limit_with_ties: false,
9727                    window_check_exprs: Vec::new(),
9728                }))
9729            };
9730            let refs = |e: &Expr| expr_refs_tables(e, &names);
9731            if let Some(items) = returning.as_mut() {
9732                for item in items.iter_mut() {
9733                    if let SelectItem::Expr { expr, .. } = item {
9734                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9735                    }
9736                }
9737            }
9738            // A LEFT join deletes the target rows the WHERE selects, reading
9739            // source columns through the correlated subquery (NULL when
9740            // unmatched); no EXISTS row filter.
9741            if mysql_outer {
9742                let mut outer = where_;
9743                if let Some(w) = outer.as_mut() {
9744                    wrap_from_leaves(w, &names, &make_subq, &refs);
9745                }
9746                outer
9747            } else {
9748                Some(Expr::Exists {
9749                    subquery: Box::new(SelectStatement {
9750                        locking: None,
9751                        ctes: Vec::new(),
9752                        distinct: false,
9753                        distinct_on: Vec::new(),
9754                        items: alloc::vec![SelectItem::Expr {
9755                            expr: Expr::Literal(Literal::Integer(1)),
9756                            alias: None,
9757                        }],
9758                        from: Some(fc),
9759                        where_: exists_where,
9760                        group_by: None,
9761                        group_by_all: false,
9762                        having: None,
9763                        unions: Vec::new(),
9764                        order_by: Vec::new(),
9765                        limit: None,
9766                        offset: None,
9767                        limit_with_ties: false,
9768                        window_check_exprs: Vec::new(),
9769                    }),
9770                    negated: false,
9771                })
9772            }
9773        } else {
9774            where_
9775        };
9776        Ok(Statement::Delete(crate::ast::DeleteStatement {
9777            ctes: Vec::new(),
9778            table,
9779            only,
9780            alias,
9781            where_,
9782            order_limit: delete_order_limit,
9783            returning,
9784        }))
9785    }
9786
9787    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9788    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9789    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9790    /// keyword. v7.17 surface:
9791    ///   * source: table reference (subquery source is a follow-up)
9792    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9793    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9794    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9795    ///     order
9796    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9797        // INTO
9798        let is_into_kw = matches!(self.peek(), Token::Into)
9799            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9800        if !is_into_kw {
9801            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9802        }
9803        self.advance();
9804        let target = self.expect_ident_like()?;
9805        // Optional alias — bare ident before USING.
9806        let target_alias = match self.peek() {
9807            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9808                Some(self.expect_ident_like()?)
9809            }
9810            _ => None,
9811        };
9812        // USING
9813        let is_using_kw = matches!(
9814            self.peek(),
9815            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9816        );
9817        if !is_using_kw {
9818            return Err(self.err(format!(
9819                "expected USING after MERGE INTO target, got {:?}",
9820                self.peek()
9821            )));
9822        }
9823        self.advance();
9824        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9825        // <table> [alias]`. PG requires an alias after a subquery source.
9826        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9827            self.advance(); // (
9828            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9829            // constant-SELECT lowering the derived-table parser uses
9830            // (PG deletes through this form; it was a parse error).
9831            let inner = if matches!(self.peek(), Token::Values) {
9832                self.advance(); // VALUES
9833                Statement::Select(self.parse_values_rows_body()?)
9834            } else {
9835                self.parse_select_stmt()?
9836            };
9837            match self.advance() {
9838                Token::RParen => {}
9839                other => {
9840                    return Err(self.err(format!(
9841                        "expected ')' after MERGE USING subquery, got {other:?}"
9842                    )));
9843                }
9844            }
9845            let Statement::Select(sub) = inner else {
9846                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9847            };
9848            (String::new(), Some(Box::new(sub)))
9849        } else {
9850            (self.expect_ident_like()?, None)
9851        };
9852        let source_alias = match self.peek() {
9853            Token::Ident(s) | Token::QuotedIdent(s)
9854                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9855            {
9856                Some(self.expect_ident_like()?)
9857            }
9858            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9859                self.advance(); // AS
9860                Some(self.expect_ident_like()?)
9861            }
9862            _ => None,
9863        };
9864        // v7.39 (round 768, F31-D5) — optional positional column-alias
9865        // list after the source alias (`s(id, v)`).
9866        let mut source_column_aliases: Vec<String> = Vec::new();
9867        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9868            self.advance();
9869            loop {
9870                source_column_aliases.push(self.expect_ident_like()?);
9871                match self.peek() {
9872                    Token::Comma => {
9873                        self.advance();
9874                    }
9875                    Token::RParen => {
9876                        self.advance();
9877                        break;
9878                    }
9879                    other => {
9880                        return Err(self.err(format!(
9881                            "expected ',' or ')' in MERGE source column list, got {other:?}"
9882                        )));
9883                    }
9884                }
9885            }
9886        }
9887        if source_select.is_some() && source_alias.is_none() {
9888            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9889        }
9890        // ON
9891        if !matches!(self.peek(), Token::On) {
9892            return Err(self.err(format!(
9893                "expected ON after MERGE … USING source, got {:?}",
9894                self.peek()
9895            )));
9896        }
9897        self.advance();
9898        let on = self.parse_expr(0)?;
9899        // One or more WHEN clauses.
9900        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9901        loop {
9902            let is_when_kw = matches!(
9903                self.peek(),
9904                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9905            );
9906            if !is_when_kw {
9907                break;
9908            }
9909            self.advance(); // WHEN
9910            // [NOT] MATCHED
9911            let matched = if matches!(self.peek(), Token::Not) {
9912                self.advance();
9913                crate::ast::MergeMatched::NotMatched
9914            } else {
9915                crate::ast::MergeMatched::Matched
9916            };
9917            let is_matched_kw = matches!(
9918                self.peek(),
9919                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9920            );
9921            if !is_matched_kw {
9922                return Err(self.err(format!(
9923                    "expected MATCHED in WHEN clause, got {:?}",
9924                    self.peek()
9925                )));
9926            }
9927            self.advance();
9928            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9929            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9930            // to fire for target rows no source row matches.
9931            let mut matched = matched;
9932            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9933                self.advance();
9934                match self.peek() {
9935                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9936                        self.advance();
9937                        matched = crate::ast::MergeMatched::NotMatchedBySource;
9938                    }
9939                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9940                        self.advance();
9941                    }
9942                    other => {
9943                        return Err(self.err(format!(
9944                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9945                        )));
9946                    }
9947                }
9948            }
9949            // Optional AND <expr>
9950            let condition = if matches!(self.peek(), Token::And) {
9951                self.advance();
9952                Some(self.parse_expr(0)?)
9953            } else {
9954                None
9955            };
9956            // THEN
9957            let is_then_kw = matches!(
9958                self.peek(),
9959                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9960            );
9961            if !is_then_kw {
9962                return Err(self.err(format!(
9963                    "expected THEN in WHEN clause, got {:?}",
9964                    self.peek()
9965                )));
9966            }
9967            self.advance();
9968            // Action: INSERT / UPDATE / DELETE / DO NOTHING
9969            let action = match self.peek().clone() {
9970                Token::Insert => {
9971                    self.advance();
9972                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9973                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9974                    // VALUES (…)` omits it and fills every column in declaration
9975                    // order. PG accepts this; SPG used to require the list.
9976                    let mut columns: Vec<String> = Vec::new();
9977                    if matches!(self.peek(), Token::LParen) {
9978                        self.advance();
9979                        loop {
9980                            columns.push(self.expect_ident_like()?);
9981                            if matches!(self.peek(), Token::Comma) {
9982                                self.advance();
9983                                continue;
9984                            }
9985                            break;
9986                        }
9987                        if !matches!(self.peek(), Token::RParen) {
9988                            return Err(self.err(format!(
9989                                "expected ')' after INSERT column list, got {:?}",
9990                                self.peek()
9991                            )));
9992                        }
9993                        self.advance();
9994                    }
9995                    // VALUES (...)
9996                    if !matches!(self.peek(), Token::Values) {
9997                        return Err(self.err(format!(
9998                            "expected VALUES in MERGE INSERT, got {:?}",
9999                            self.peek()
10000                        )));
10001                    }
10002                    self.advance();
10003                    if !matches!(self.peek(), Token::LParen) {
10004                        return Err(self.err(format!(
10005                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10006                            self.peek()
10007                        )));
10008                    }
10009                    self.advance();
10010                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10011                    loop {
10012                        values.push(self.parse_expr(0)?);
10013                        if matches!(self.peek(), Token::Comma) {
10014                            self.advance();
10015                            continue;
10016                        }
10017                        break;
10018                    }
10019                    if !matches!(self.peek(), Token::RParen) {
10020                        return Err(self.err(format!(
10021                            "expected ')' after MERGE INSERT values, got {:?}",
10022                            self.peek()
10023                        )));
10024                    }
10025                    self.advance();
10026                    // Empty column list = positional into every column, so the
10027                    // count is checked against the table arity at execution.
10028                    if !columns.is_empty() && columns.len() != values.len() {
10029                        return Err(self.err(format!(
10030                            "MERGE INSERT column count ({}) ≠ value count ({})",
10031                            columns.len(),
10032                            values.len()
10033                        )));
10034                    }
10035                    crate::ast::MergeAction::Insert { columns, values }
10036                }
10037                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10038                    self.advance();
10039                    // SET
10040                    let is_set_kw = matches!(
10041                        self.peek(),
10042                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10043                    );
10044                    if !is_set_kw {
10045                        return Err(self.err(format!(
10046                            "expected SET after UPDATE in MERGE, got {:?}",
10047                            self.peek()
10048                        )));
10049                    }
10050                    self.advance();
10051                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10052                    loop {
10053                        let col = self.expect_ident_like()?;
10054                        if !matches!(self.peek(), Token::Eq) {
10055                            return Err(self.err(format!(
10056                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10057                                self.peek()
10058                            )));
10059                        }
10060                        self.advance();
10061                        let expr = self.parse_expr(0)?;
10062                        assignments.push((col, expr));
10063                        if matches!(self.peek(), Token::Comma) {
10064                            self.advance();
10065                            continue;
10066                        }
10067                        break;
10068                    }
10069                    crate::ast::MergeAction::Update { assignments }
10070                }
10071                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10072                    self.advance();
10073                    crate::ast::MergeAction::Delete
10074                }
10075                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10076                    self.advance();
10077                    let is_nothing_kw = matches!(
10078                        self.peek(),
10079                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10080                    );
10081                    if !is_nothing_kw {
10082                        return Err(self.err(format!(
10083                            "expected NOTHING after DO in MERGE clause, got {:?}",
10084                            self.peek()
10085                        )));
10086                    }
10087                    self.advance();
10088                    crate::ast::MergeAction::DoNothing
10089                }
10090                other => {
10091                    return Err(self.err(format!(
10092                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10093                    )));
10094                }
10095            };
10096            // PG's grammar simply has no INSERT production under BY SOURCE
10097            // (a target row already exists there) — same syntax error.
10098            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10099                && matches!(action, crate::ast::MergeAction::Insert { .. })
10100            {
10101                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10102            }
10103            clauses.push(crate::ast::MergeWhenClause {
10104                matched,
10105                condition,
10106                action,
10107            });
10108        }
10109        if clauses.is_empty() {
10110            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10111        }
10112        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10113        // unconditional (no `AND`) WHEN of the same match kind: it could
10114        // never fire. Check per match kind in clause order.
10115        let mut seen_unconditional_matched = false;
10116        let mut seen_unconditional_not_matched = false;
10117        let mut seen_unconditional_by_source = false;
10118        for c in &clauses {
10119            let seen = match c.matched {
10120                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10121                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10122                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10123            };
10124            if *seen {
10125                return Err(self.err(String::from(
10126                    "unreachable WHEN clause specified after unconditional WHEN clause",
10127                )));
10128            }
10129            if c.condition.is_none() {
10130                *seen = true;
10131            }
10132        }
10133        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10134        let returning = self.parse_optional_returning()?;
10135        Ok(Statement::Merge(crate::ast::MergeStatement {
10136            // Attached by `parse_with_cte_then_select` when the MERGE
10137            // heads a WITH clause (round 149).
10138            ctes: Vec::new(),
10139            target,
10140            target_alias,
10141            source,
10142            source_alias,
10143            source_select,
10144            source_column_aliases,
10145            on,
10146            clauses,
10147            returning,
10148        }))
10149    }
10150
10151    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10152    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10153    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10154    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10155    fn parse_optional_returning(
10156        &mut self,
10157    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10158        let is_returning_kw = matches!(
10159            self.peek(),
10160            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10161        );
10162        if !is_returning_kw {
10163            return Ok(None);
10164        }
10165        self.advance();
10166        let mut items = Vec::new();
10167        loop {
10168            items.push(self.parse_select_item()?);
10169            if matches!(self.peek(), Token::Comma) {
10170                self.advance();
10171                continue;
10172            }
10173            break;
10174        }
10175        Ok(Some(items))
10176    }
10177
10178    /// v6.0.4 — parse the tail of an ALTER statement after the
10179    /// leading `ALTER` keyword has been consumed. Only one form is
10180    /// supported in v6.0.4:
10181    ///
10182    /// ```text
10183    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10184    /// ```
10185    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10186        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10187        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10188        // exclusion) is accepted by stripping the `ONLY` keyword
10189        // before the table parse.
10190        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10191        // and the long PG-dump tail are accepted as no-ops.
10192        match self.advance() {
10193            Token::Index => {}
10194            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10195            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10196            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10197            Token::Table => {
10198                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10199                    self.advance();
10200                }
10201                return self.parse_alter_table_after_keyword();
10202            }
10203            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10204                return self.parse_alter_policy_after_keyword();
10205            }
10206            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10207                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10208                    self.advance();
10209                }
10210                return self.parse_alter_table_after_keyword();
10211            }
10212            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10213            // of the silent-noop tail.
10214            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10215                return self.parse_alter_sequence_after_keyword();
10216            }
10217            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10218            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10219            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10220            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10221                // NB: the match arm consumed `TYPE` via self.advance(); the
10222                // cursor is now at the type name — do NOT advance again.
10223                let type_name = self.expect_ident_like()?;
10224                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10225                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10226                if is_add_value {
10227                    self.advance(); // ADD
10228                    self.advance(); // VALUE
10229                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10230                    // IF/EXISTS as identifiers.
10231                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10232                    {
10233                        let n1 = self.tokens.get(self.pos + 1);
10234                        let n2 = self.tokens.get(self.pos + 2);
10235                        if matches!(n1, Some(Token::Not))
10236                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10237                        {
10238                            self.advance();
10239                            self.advance();
10240                            self.advance();
10241                            true
10242                        } else {
10243                            false
10244                        }
10245                    } else {
10246                        false
10247                    };
10248                    let label = self.expect_string_literal()?;
10249                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10250                    {
10251                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10252                        self.advance();
10253                        let anchor = self.expect_string_literal()?;
10254                        Some((is_before, anchor))
10255                    } else {
10256                        None
10257                    };
10258                    return Ok(Statement::AlterTypeAddValue {
10259                        type_name,
10260                        label,
10261                        if_not_exists,
10262                        position,
10263                    });
10264                }
10265                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10266                // Used to fall into the no-op tail below: accepted, silently
10267                // ignored. `RENAME TO <newtype>` keeps falling through.
10268                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10269                    && matches!(
10270                        self.tokens.get(self.pos + 1),
10271                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10272                    )
10273                {
10274                    self.advance(); // RENAME
10275                    self.advance(); // VALUE
10276                    let old = self.expect_string_literal()?;
10277                    if matches!(self.peek(), Token::To) {
10278                        self.advance();
10279                    } else {
10280                        self.expect_keyword_ident("to")?;
10281                    }
10282                    let new = self.expect_string_literal()?;
10283                    return Ok(Statement::AlterTypeRenameValue {
10284                        type_name,
10285                        old,
10286                        new,
10287                    });
10288                }
10289                // Other ALTER TYPE forms — the ACTION stays a no-op
10290                // (pg_dump tail), but v7.39 (round 708) the NAME is
10291                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10292                // success for a type that does not exist.
10293                self.consume_until_statement_boundary();
10294                return Ok(Statement::ValidateOnly {
10295                    kind: crate::ast::ValidateOnlyKind::TypeName,
10296                    names: alloc::vec![type_name],
10297                });
10298            }
10299            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10300            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10301            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10302            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10303            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10304            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10305            // pg_dump no-op list below: every form used to report success
10306            // and change nothing, which is worse than refusing outright
10307            // (a migration dropping a constraint kept rejecting data).
10308            // NOTE: the enclosing `match self.advance()` already consumed
10309            // the DOMAIN keyword, so the name is next.
10310            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10311                return self.parse_alter_domain_after_keyword();
10312            }
10313            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10314            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10315            // used to fall into the pg_dump no-op tail below, so a DBA
10316            // setting a per-role default was told it worked and nothing
10317            // happened. Intercepted here, BEFORE that tail.
10318            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10319            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10320            // interception below exists: swallowed with the no-op tail, an
10321            // unknown parameter name was ACCEPTED where PG18 answers
10322            // `unrecognized configuration parameter`. SPG applies nothing
10323            // either way — there is no postgresql.auto.conf — but it now
10324            // says so about a name it does not know.
10325            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10326                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10327                // already consumed here. An extra advance eats the SET and
10328                // the parameter name is never seen — which is exactly the
10329                // bug a panic in this branch disproved: the branch WAS on
10330                // the path, the reading of it was wrong.
10331                let mut parameter = None;
10332                // SET <name> … | RESET <name> | RESET ALL
10333                if matches!(self.peek(), Token::Ident(k)
10334                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10335                {
10336                    self.advance();
10337                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10338                        && !n.eq_ignore_ascii_case("all")
10339                    {
10340                        self.advance();
10341                        // A dotted GUC (`plpgsql.check_asserts`) is two
10342                        // tokens; keep the whole name.
10343                        let mut full = n;
10344                        while matches!(self.peek(), Token::Dot) {
10345                            self.advance();
10346                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10347                                full.push('.');
10348                                full.push_str(&t);
10349                            }
10350                        }
10351                        parameter = Some(full);
10352                    }
10353                }
10354                self.consume_until_statement_boundary();
10355                return Ok(Statement::AlterSystem { parameter });
10356            }
10357            Token::Ident(s) | Token::QuotedIdent(s)
10358                if matches!(
10359                    s.to_ascii_lowercase().as_str(),
10360                    "role" | "user" | "database"
10361                ) && self.peeks_db_role_setting() =>
10362            {
10363                let is_database = s.eq_ignore_ascii_case("database");
10364                return self.parse_db_role_setting(is_database);
10365            }
10366            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10367            // (the non-SET forms; SET/RESET took the branch above). The
10368            // attributes still no-op — recorded, and the ignored PASSWORD
10369            // is ledgered as its own follow-up — but the ROLE is validated:
10370            // any name was accepted for a role that does not exist.
10371            Token::Ident(s) | Token::QuotedIdent(s)
10372                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10373            {
10374                // NB: the enclosing `match self.advance()` already consumed
10375                // ROLE/USER — the round-695 trap, hit again in this round's
10376                // first draft (the name was eaten and WITH parsed as the
10377                // role). The cursor is at the name.
10378                let name = self.expect_ident_or_string()?;
10379                // v7.39 (round 750) — scan the attribute tail for
10380                // PASSWORD. Everything else stays a recorded no-op, but
10381                // a dropped credential rotation is a SECURITY bug:
10382                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10383                // changed nothing, so the old password kept working.
10384                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10385                // NULL` clears the credential.
10386                let mut password: Option<Option<String>> = None;
10387                loop {
10388                    match self.peek() {
10389                        Token::Semicolon | Token::Eof => break,
10390                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10391                            self.advance();
10392                            match self.advance() {
10393                                Token::String(p) => password = Some(Some(p)),
10394                                Token::Null => password = Some(None),
10395                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10396                                    password = Some(None);
10397                                }
10398                                other => {
10399                                    return Err(self.err(alloc::format!(
10400                                        "expected password string or NULL after PASSWORD, got {other:?}"
10401                                    )));
10402                                }
10403                            }
10404                        }
10405                        _ => {
10406                            self.advance();
10407                        }
10408                    }
10409                }
10410                if name.eq_ignore_ascii_case("all") {
10411                    // `ALTER ROLE ALL …` names every role; nothing to check.
10412                    return Ok(Statement::Empty);
10413                }
10414                if let Some(pw) = password {
10415                    return Ok(Statement::AlterRolePassword { name, password: pw });
10416                }
10417                return Ok(Statement::ValidateOnly {
10418                    kind: crate::ast::ValidateOnlyKind::RoleName,
10419                    names: alloc::vec![name],
10420                });
10421            }
10422            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10423            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10424            // list far enough to validate the NAME; the actions still no-op.
10425            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10426            // models none of them and their dumps are rare.)
10427            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10428                let name = self.expect_ident_or_string()?;
10429                self.consume_until_statement_boundary();
10430                return Ok(Statement::ValidateOnly {
10431                    kind: crate::ast::ValidateOnlyKind::CollationName,
10432                    names: alloc::vec![name],
10433                });
10434            }
10435            Token::Ident(s) | Token::QuotedIdent(s)
10436                if s.eq_ignore_ascii_case("text")
10437                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10438                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10439            {
10440                self.advance(); // SEARCH
10441                self.advance(); // CONFIGURATION
10442                let name = self.expect_ident_like()?;
10443                self.consume_until_statement_boundary();
10444                return Ok(Statement::ValidateOnly {
10445                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10446                    names: alloc::vec![name],
10447                });
10448            }
10449            Token::Ident(s) | Token::QuotedIdent(s)
10450                if s.eq_ignore_ascii_case("event")
10451                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10452            {
10453                self.advance(); // TRIGGER
10454                let name = self.expect_ident_like()?;
10455                self.consume_until_statement_boundary();
10456                return Ok(Statement::ValidateOnly {
10457                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10458                    names: alloc::vec![name],
10459                });
10460            }
10461            Token::Ident(s) | Token::QuotedIdent(s)
10462                if s.eq_ignore_ascii_case("large")
10463                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10464            {
10465                self.advance(); // OBJECT
10466                let oid = match self.advance() {
10467                    Token::Integer(n) => alloc::format!("{n}"),
10468                    other => {
10469                        return Err(
10470                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10471                        );
10472                    }
10473                };
10474                self.consume_until_statement_boundary();
10475                return Ok(Statement::ValidateOnly {
10476                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10477                    names: alloc::vec![oid],
10478                });
10479            }
10480            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10481            // argument-list parse as DROP AGGREGATE (round 707); the
10482            // action no-ops, the existence check is real.
10483            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10484                // Same round-695 trap as above: AGGREGATE is already
10485                // consumed; the cursor is at the name.
10486                let name = self.expect_ident_like()?;
10487                let mut names = alloc::vec![name];
10488                if matches!(self.peek(), Token::LParen) {
10489                    self.advance();
10490                    loop {
10491                        match self.peek().clone() {
10492                            Token::RParen => {
10493                                self.advance();
10494                                break;
10495                            }
10496                            Token::Star => {
10497                                self.advance();
10498                                names.push(String::from("*"));
10499                            }
10500                            Token::Comma => {
10501                                self.advance();
10502                            }
10503                            _ => {
10504                                let mut t = self.expect_ident_like()?;
10505                                while let Token::Ident(nx) = self.peek() {
10506                                    let nx = nx.clone();
10507                                    self.advance();
10508                                    t.push(' ');
10509                                    t.push_str(&nx);
10510                                }
10511                                names.push(t);
10512                            }
10513                        }
10514                    }
10515                }
10516                self.consume_until_statement_boundary();
10517                return Ok(Statement::ValidateOnly {
10518                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10519                    names,
10520                });
10521            }
10522            Token::Ident(s) | Token::QuotedIdent(s)
10523                if matches!(
10524                    s.to_ascii_lowercase().as_str(),
10525                    "view"
10526                        | "function"
10527                        | "database"
10528                        | "schema"
10529                        | "owner"
10530                        | "default"
10531                        | "extension"
10532                        | "materialized"
10533                        | "publication"
10534                        | "subscription"
10535                        // v7.37.17 (17.6 siblings) — additional ALTER
10536                        // targets pg_dump / pg_dumpall / operator DB
10537                        // migration scripts commonly emit. SPG has
10538                        // no matching machinery for any of these; the
10539                        // parser accepts + Empty-returns so pg_dump
10540                        // tail statements don't stall.
10541                        | "tablespace"
10542                        | "language"
10543                        | "operator"
10544                        | "conversion"
10545                        | "statistics"
10546                        | "server"
10547                        | "foreign"
10548                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10549                        // / TEMPLATE (CONFIGURATION intercepted above).
10550                        | "text"
10551                ) =>
10552            {
10553                self.consume_until_statement_boundary();
10554                return Ok(Statement::Empty);
10555            }
10556            other => {
10557                return Err(self.err(format!(
10558                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10559                     after ALTER, got {other:?}"
10560                )));
10561            }
10562        }
10563        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10564        // (mailrs migrate-042 ships these). The presence of an
10565        // IF EXISTS makes the subsequent name lookup tolerate
10566        // a missing index — engine returns CommandOk no-op.
10567        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10568            let next = self.tokens.get(self.pos + 1);
10569            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10570                self.advance();
10571                self.advance();
10572                true
10573            } else {
10574                false
10575            }
10576        } else {
10577            false
10578        };
10579        let name = self.expect_ident_like()?;
10580        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10581        // Detect BEFORE the REBUILD path so the existing REBUILD
10582        // arm stays untouched.
10583        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10584            self.advance();
10585            if matches!(self.peek(), Token::To) {
10586                self.advance();
10587            } else {
10588                self.expect_keyword_ident("to")?;
10589            }
10590            let new = self.expect_ident_like()?;
10591            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10592                name,
10593                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10594            }));
10595        }
10596        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10597        // A syntax error before; the index is validated, the params no-op.
10598        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10599            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10600                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10601        {
10602            self.consume_until_statement_boundary();
10603            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10604                name,
10605                target: crate::ast::AlterIndexTarget::StorageParams,
10606            }));
10607        }
10608        // REBUILD
10609        self.expect_keyword_ident("rebuild")?;
10610        // Optional: WITH (encoding = <enc>)
10611        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10612            self.advance();
10613            if !matches!(self.peek(), Token::LParen) {
10614                return Err(self.err(format!(
10615                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10616                    self.peek()
10617                )));
10618            }
10619            self.advance();
10620            self.expect_keyword_ident("encoding")?;
10621            if !matches!(self.peek(), Token::Eq) {
10622                return Err(self.err(format!(
10623                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10624                    self.peek()
10625                )));
10626            }
10627            self.advance();
10628            let enc_ident = match self.advance() {
10629                Token::Ident(s) | Token::QuotedIdent(s) => s,
10630                other => {
10631                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10632                }
10633            };
10634            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10635                "f32" => VecEncoding::F32,
10636                "sq8" => VecEncoding::Sq8,
10637                "half" => VecEncoding::F16,
10638                other => {
10639                    return Err(self.err(format!(
10640                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10641                    )));
10642                }
10643            };
10644            if !matches!(self.peek(), Token::RParen) {
10645                return Err(self.err(format!(
10646                    "expected ')' after encoding value, got {:?}",
10647                    self.peek()
10648                )));
10649            }
10650            self.advance();
10651            Some(enc)
10652        } else {
10653            None
10654        };
10655        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10656            name,
10657            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10658        }))
10659    }
10660
10661    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10662    /// only `SET` form currently supported; future v6.7.x can add
10663    /// more SET subjects without changing the dispatch shape.
10664    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10665    /// subactions. Single-subaction shape stays a 1-element vec.
10666    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10667        let table_name = self.expect_ident_like()?;
10668        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10669        loop {
10670            let subaction = self.parse_alter_table_subaction()?;
10671            // ADD COLUMN with inline REFERENCES emits both an
10672            // AddColumn and an AddForeignKey subaction; the
10673            // helper returns 1 or 2 items.
10674            targets.extend(subaction);
10675            if matches!(self.peek(), Token::Comma) {
10676                self.advance();
10677                continue;
10678            }
10679            break;
10680        }
10681        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10682            name: table_name,
10683            targets,
10684        }))
10685    }
10686
10687    /// Parse one ALTER TABLE subaction. Returns a Vec because
10688    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10689    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10690    fn parse_alter_table_subaction(
10691        &mut self,
10692    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10693        match self.peek() {
10694            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10695                self.advance();
10696                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10697                // storage parameters: paren-prefixed; consume.
10698                if matches!(self.peek(), Token::LParen) {
10699                    self.consume_until_statement_boundary();
10700                    return Ok(Vec::new());
10701                }
10702                let setting = self.expect_ident_like()?;
10703                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10704                    if !matches!(self.peek(), Token::Eq) {
10705                        return Err(self.err(alloc::format!(
10706                            "expected '=' after hot_tier_bytes, got {:?}",
10707                            self.peek()
10708                        )));
10709                    }
10710                    self.advance();
10711                    let n = self.expect_u64_literal()?;
10712                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10713                }
10714                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10715                // accept-and-no-op for ALTER TABLE SET <subject>
10716                // forms that pg_dump emits but SPG either treats
10717                // as N/A (single-tenant, single-owner, no shared
10718                // tablespaces) or accepts the dump-side declaration
10719                // without runtime effect:
10720                //   SET SCHEMA <name>            (18.11)
10721                //   SET TABLESPACE <name>        (18.8)
10722                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10723                //   SET WITHOUT CLUSTER          (18.13)
10724                //   SET WITHOUT OIDS             (PG legacy)
10725                //   SET (option = value, …)      (storage parameters)
10726                //   SET REPLICA IDENTITY {…}     (18.14)
10727                if setting.eq_ignore_ascii_case("schema")
10728                    || setting.eq_ignore_ascii_case("tablespace")
10729                    || setting.eq_ignore_ascii_case("logged")
10730                    || setting.eq_ignore_ascii_case("unlogged")
10731                    || setting.eq_ignore_ascii_case("without")
10732                {
10733                    self.consume_until_statement_boundary();
10734                    return Ok(Vec::new());
10735                }
10736                if setting.eq_ignore_ascii_case("replica") {
10737                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10738                    self.consume_until_statement_boundary();
10739                    return Ok(Vec::new());
10740                }
10741                // SET (option=value, …) — storage parameters.
10742                if matches!(self.peek(), Token::LParen) {
10743                    self.consume_until_statement_boundary();
10744                    return Ok(Vec::new());
10745                }
10746                Err(self.err(alloc::format!(
10747                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10748                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10749                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10750                )))
10751            }
10752            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10753            // not ignored: round 645 gave SPG the inheritance the
10754            // v7.37.18 no-op said it did not have.
10755            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10756                self.advance();
10757                let parent = self.expect_ident_like()?;
10758                self.consume_until_statement_boundary();
10759                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10760                    parent,
10761                    detach: false
10762                }])
10763            }
10764            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10765            // LEVEL SECURITY`, which has its own RLS arm below — without
10766            // the guard this swallowed NO FORCE as a no-op.
10767            Token::Ident(s)
10768                if s.eq_ignore_ascii_case("no")
10769                    && !matches!(
10770                        self.tokens.get(self.pos + 1),
10771                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10772                    ) =>
10773            {
10774                self.advance();
10775                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10776                    if k.eq_ignore_ascii_case("inherit"))
10777                {
10778                    self.advance();
10779                    let parent = self.expect_ident_like()?;
10780                    self.consume_until_statement_boundary();
10781                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10782                        parent,
10783                        detach: true
10784                    }]);
10785                }
10786                self.consume_until_statement_boundary();
10787                Ok(Vec::new())
10788            }
10789            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10790            // single-owner, so there is still nothing to record.
10791            //
10792            // v7.39 (round 652) — but the name now reaches the engine,
10793            // which refuses a role that does not exist as PG does. The
10794            // no-op was swallowing the whole statement, so a dump naming
10795            // a role this server never heard of restored clean and left
10796            // the table owned by whoever ran the restore.
10797            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10798                self.advance();
10799                if matches!(self.peek(), Token::To) {
10800                    self.advance();
10801                }
10802                let role = self.expect_ident_like()?;
10803                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10804                    role
10805                }])
10806            }
10807            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10808            // PG sets a hint; SPG doesn't have clustered storage, so the
10809            // hint itself stays a no-op.
10810            //
10811            // v7.39 (round 652) — the index name is checked now. PG
10812            // errors on one that does not exist, and swallowing that let
10813            // a typo'd CLUSTER ON pass silently.
10814            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10815                self.advance();
10816                // `ON` is a reserved token, not an ident.
10817                if !matches!(self.peek(), Token::On) {
10818                    return Err(self.err(alloc::format!(
10819                        "expected ON after CLUSTER, got {:?}",
10820                        self.peek()
10821                    )));
10822                }
10823                self.advance();
10824                let index = self.expect_ident_like()?;
10825                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10826                    index: Some(index)
10827                }])
10828            }
10829            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10830            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10831            // what a logical decoder puts in the old-tuple image; SPG's
10832            // replication is SQL-text, so there is nothing to record.
10833            // Accept-and-no-op (it used to be a parse error).
10834            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10835                self.advance();
10836                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10837                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10838                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10839                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10840                {
10841                    self.advance(); // IDENTITY
10842                    self.advance(); // USING
10843                    if matches!(self.peek(), Token::Index)
10844                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10845                    {
10846                        self.advance();
10847                    }
10848                    let index = self.expect_ident_like()?;
10849                    self.consume_until_statement_boundary();
10850                    return Ok(alloc::vec![
10851                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10852                    ]);
10853                }
10854                self.consume_until_statement_boundary();
10855                Ok(Vec::new())
10856            }
10857            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10858            //
10859            // v7.39 (round 652) — it used to consume the statement and
10860            // return nothing, on the stated theory that SPG validated at
10861            // ADD CONSTRAINT time so there was never anything left to
10862            // validate. Measured against PG18, ADD CONSTRAINT did not
10863            // scan the existing rows at all — the comment described a
10864            // property SPG did not have, which is why nobody looked. Both
10865            // halves are real now: ADD scans unless told NOT VALID, and
10866            // this scans what NOT VALID skipped.
10867            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10868                self.advance();
10869                self.expect_keyword_ident("constraint")?;
10870                let name = self.expect_ident_like()?;
10871                Ok(alloc::vec![
10872                    crate::ast::AlterTableTarget::ValidateConstraint { name }
10873                ])
10874            }
10875            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10876            // SET (option = value, …). PG uses it to clear per-table
10877            // storage params like fillfactor or autovacuum_*. SPG
10878            // engine-manages those parameters; accept-and-no-op.
10879            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10880                self.advance();
10881                self.consume_until_statement_boundary();
10882                Ok(Vec::new())
10883            }
10884            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10885            // type-of binding (PG 9.0+). SPG composite types
10886            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10887            // TABLE OF is rare and inverse of CREATE TABLE OF.
10888            // Accept-and-no-op until a customer dump round-trips it.
10889            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10890                self.advance();
10891                // v7.39 (round 710) — the type name is validated now.
10892                let type_name = self.expect_ident_like()?;
10893                self.consume_until_statement_boundary();
10894                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10895                    type_name
10896                }])
10897            }
10898            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10899            // (reserved keyword) rather than Token::Ident("not"),
10900            // so it needs its own arm. Accept-and-no-op same as OF.
10901            Token::Not => {
10902                self.advance();
10903                self.consume_until_statement_boundary();
10904                Ok(Vec::new())
10905            }
10906            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10907            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10908                self.advance();
10909                self.expect_row_level_security()?;
10910                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10911                    enabled: None,
10912                    force: Some(true),
10913                }])
10914            }
10915            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10916            Token::Ident(s)
10917                if s.eq_ignore_ascii_case("no")
10918                    && matches!(
10919                        self.tokens.get(self.pos + 1),
10920                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10921                    ) =>
10922            {
10923                self.advance(); // NO
10924                self.advance(); // FORCE
10925                self.expect_row_level_security()?;
10926                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10927                    enabled: None,
10928                    force: Some(false),
10929                }])
10930            }
10931            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10932            // (sets relrowsecurity). The guard requires the next token to be
10933            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10934            Token::Ident(s)
10935                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10936                    && matches!(
10937                        self.tokens.get(self.pos + 1),
10938                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10939                    ) =>
10940            {
10941                let enabled = s.eq_ignore_ascii_case("enable");
10942                self.advance(); // ENABLE/DISABLE
10943                self.expect_row_level_security()?;
10944                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10945                    enabled: Some(enabled),
10946                    force: None,
10947                }])
10948            }
10949            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10950                self.advance();
10951                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10952                // {INDEX|KEY} [name] (cols)`, which every ORM migration
10953                // emits. The same grammar CREATE TABLE already accepts
10954                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10955                // through the SAME parser — an ALTER-only copy would be a
10956                // second place for the two to drift.
10957                if self.peek_mysql_inline_key_start() {
10958                    return Ok(match self.parse_mysql_inline_key()? {
10959                        Some(c) => {
10960                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10961                        }
10962                        // FULLTEXT / SPATIAL parse and are accepted as a
10963                        // no-op here exactly as they are inline.
10964                        None => Vec::new(),
10965                    });
10966                }
10967                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10968                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10969                // PRIMARY KEY this way; mysqldump emits both.
10970                // Peek-only dispatch (no advance) — `advance()`
10971                // destructively replaces consumed tokens with Eof,
10972                // so saved-pos restore would land on Eofs.
10973                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10974                {
10975                    // The next-but-one ident is the constraint
10976                    // name; the one after THAT is the kind.
10977                    let kind_pos = self.pos + 2;
10978                    let kind = self.tokens.get(kind_pos).cloned();
10979                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10980                    {
10981                        let fk = self.parse_table_level_fk()?;
10982                        return Ok(alloc::vec![
10983                            crate::ast::AlterTableTarget::AddForeignKey(fk)
10984                        ]);
10985                    }
10986                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10987                    {
10988                        self.advance(); // CONSTRAINT
10989                        // v7.39 (read01 round 48) — keep the name; the engine
10990                        // stores it now instead of dropping it on the floor.
10991                        let con_name = self.expect_ident_like()?;
10992                        self.advance(); // PRIMARY
10993                        self.expect_keyword_ident("key")?;
10994                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10995                        // v7.39 (round 711) — the ALTER form carries the
10996                        // timing too (pg_dump writes it here).
10997                        let (deferrable, initially_deferred) =
10998                            self.consume_deferrable_clauses_timed()?;
10999                        return Ok(alloc::vec![
11000                            crate::ast::AlterTableTarget::AddTableConstraint(
11001                                crate::ast::TableConstraint::PrimaryKey {
11002                                    name: Some(con_name),
11003                                    columns: cols,
11004                                    deferrable,
11005                                    initially_deferred,
11006                                }
11007                            )
11008                        ]);
11009                    }
11010                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11011                    {
11012                        self.advance(); // CONSTRAINT
11013                        // v7.39 (read01 round 48) — keep the name.
11014                        let con_name = self.expect_ident_like()?;
11015                        // v7.22 (mailrs round-13 gap 6) — delegate so
11016                        // the optional `NULLS [NOT] DISTINCT` modifier
11017                        // parses here too (pg_dump emits the ALTER
11018                        // form; semantics enforced by the engine
11019                        // since v7.13).
11020                        let mut uc = self.parse_table_level_unique()?;
11021                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11022                            *name = Some(con_name);
11023                        }
11024                        return Ok(alloc::vec![
11025                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11026                        ]);
11027                    }
11028                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11029                    {
11030                        self.advance(); // CONSTRAINT
11031                        // v7.39 (read01 round 48) — keep the name.
11032                        let con_name = self.expect_ident_like()?;
11033                        self.advance(); // CHECK
11034                        if !matches!(self.peek(), Token::LParen) {
11035                            return Err(self.err(alloc::format!(
11036                                "expected '(' after CHECK, got {:?}", self.peek()
11037                            )));
11038                        }
11039                        self.advance();
11040                        let expr = self.parse_expr(0)?;
11041                        if matches!(self.peek(), Token::RParen) {
11042                            self.advance();
11043                        }
11044                        let not_valid = self.parse_not_valid_suffix();
11045                        return Ok(alloc::vec![
11046                            crate::ast::AlterTableTarget::AddTableConstraint(
11047                                crate::ast::TableConstraint::Check {
11048                                    name: Some(con_name),
11049                                    expr,
11050                                    not_valid,
11051                                }
11052                            )
11053                        ]);
11054                    }
11055                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11056                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11057                    // exclusion constraints via this ALTER form.
11058                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11059                    {
11060                        self.advance(); // CONSTRAINT
11061                        let con_name = self.expect_ident_like()?;
11062                        let mut ex = self.parse_table_level_exclude()?;
11063                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11064                            *name = Some(con_name);
11065                        }
11066                        return Ok(alloc::vec![
11067                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11068                        ]);
11069                    }
11070                    // Unknown kind — fall through to FK path which
11071                    // produces a descriptive parse error.
11072                }
11073                let is_fk = matches!(
11074                    self.peek(),
11075                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11076                        || s.eq_ignore_ascii_case("foreign")
11077                );
11078                if is_fk {
11079                    let fk = self.parse_table_level_fk()?;
11080                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11081                }
11082                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11083                // (no CONSTRAINT prefix) — same dispatch.
11084                match self.peek().clone() {
11085                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11086                        self.advance();
11087                        self.expect_keyword_ident("key")?;
11088                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11089                        let (deferrable, initially_deferred) =
11090                            self.consume_deferrable_clauses_timed()?;
11091                        return Ok(alloc::vec![
11092                            crate::ast::AlterTableTarget::AddTableConstraint(
11093                                crate::ast::TableConstraint::PrimaryKey {
11094                                    name: None,
11095                                    columns: cols,
11096                                    deferrable,
11097                                    initially_deferred,
11098                                }
11099                            )
11100                        ]);
11101                    }
11102                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11103                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11104                        let uc = self.parse_table_level_unique()?;
11105                        return Ok(alloc::vec![
11106                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11107                        ]);
11108                    }
11109                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11110                    // prefix). The other three bare forms were here and
11111                    // this one was not, so it fell through to the column
11112                    // path and came back as "unexpected reserved keyword
11113                    // 'check' at start of column definition".
11114                    _ if self.peek_table_level_check_start() => {
11115                        let chk = self.parse_table_level_check()?;
11116                        let not_valid = self.parse_not_valid_suffix();
11117                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11118                            unreachable!("parse_table_level_check returns Check")
11119                        };
11120                        return Ok(alloc::vec![
11121                            crate::ast::AlterTableTarget::AddTableConstraint(
11122                                crate::ast::TableConstraint::Check {
11123                                    name: None,
11124                                    expr,
11125                                    not_valid,
11126                                }
11127                            )
11128                        ]);
11129                    }
11130                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11131                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11132                        let ex = self.parse_table_level_exclude()?;
11133                        return Ok(alloc::vec![
11134                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11135                        ]);
11136                    }
11137                    _ => {}
11138                }
11139                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11140                    self.advance();
11141                }
11142                let mut if_not_exists = false;
11143                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11144                    self.advance();
11145                    if !matches!(self.peek(), Token::Not) {
11146                        return Err(self.err(alloc::format!(
11147                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11148                            self.peek()
11149                        )));
11150                    }
11151                    self.advance();
11152                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11153                        return Err(self.err(alloc::format!(
11154                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11155                            self.peek()
11156                        )));
11157                    }
11158                    self.advance();
11159                    if_not_exists = true;
11160                }
11161                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11162                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11163                // returns ColumnDef + an optional inline FK.
11164                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11165                let col_name = column.name.clone();
11166                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11167                    column,
11168                    if_not_exists,
11169                }];
11170                if let Some(mut fk) = col_level_fk {
11171                    if fk.columns.is_empty() {
11172                        fk.columns.push(col_name);
11173                    }
11174                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11175                }
11176                Ok(out)
11177            }
11178            Token::Drop => {
11179                self.advance();
11180                // v7.13.3 — dispatch on the next token. mailrs round-7
11181                // S8 closed DROP COLUMN; round-6 S7 closed
11182                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11183                // RESTRICT modifiers.
11184                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11185                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11186                let subject = match self.peek() {
11187                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11188                        self.advance();
11189                        "constraint"
11190                    }
11191                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11192                        self.advance();
11193                        "column"
11194                    }
11195                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11196                    // `INDEX` lexes as the reserved Token::Index, so it is
11197                    // unambiguous. `KEY` is a plain ident, and PG allows a
11198                    // column literally named "key", so only read it as the
11199                    // keyword when a name follows it.
11200                    Token::Index => {
11201                        self.advance();
11202                        "index"
11203                    }
11204                    Token::Ident(s)
11205                        if s.eq_ignore_ascii_case("key")
11206                            && matches!(
11207                                self.tokens.get(self.pos + 1),
11208                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11209                            ) =>
11210                    {
11211                        self.advance();
11212                        "index"
11213                    }
11214                    // PG-canonical bare `DROP <col>` without COLUMN
11215                    // keyword is also valid; treat any other ident
11216                    // as the column name.
11217                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11218                    other => {
11219                        return Err(self.err(alloc::format!(
11220                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11221                        )));
11222                    }
11223                };
11224                let mut if_exists = false;
11225                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11226                    let n1 = self.tokens.get(self.pos + 1);
11227                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11228                        self.advance();
11229                        self.advance();
11230                        if_exists = true;
11231                    }
11232                }
11233                let name = self.expect_ident_like()?;
11234                let mut cascade = false;
11235                if matches!(
11236                    self.peek(),
11237                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11238                        || s.eq_ignore_ascii_case("restrict")
11239                ) {
11240                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11241                    {
11242                        cascade = true;
11243                    }
11244                    self.advance();
11245                }
11246                if subject == "index" {
11247                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11248                        name,
11249                        if_exists,
11250                    }])
11251                } else if subject == "constraint" {
11252                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11253                        name,
11254                        if_exists,
11255                    }])
11256                } else {
11257                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11258                        column: name,
11259                        if_exists,
11260                        cascade,
11261                    }])
11262                }
11263            }
11264            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11265                self.advance();
11266                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11267                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11268                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11269                // immediately; accept-and-no-op.
11270                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11271                    self.advance();
11272                    self.consume_until_statement_boundary();
11273                    return Ok(Vec::new());
11274                }
11275                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11276                    self.advance();
11277                }
11278                let col_name = self.expect_ident_like()?;
11279                match self.peek() {
11280                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11281                        self.advance();
11282                    }
11283                    // v7.14.0 — pg_dump emits BIGSERIAL via
11284                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11285                    // nextval('seq')` (the sequence is created
11286                    // separately). SPG's BIGSERIAL already uses
11287                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11288                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11289                    // engine no-ops by consuming the tail.
11290                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11291                        // v7.22 (round-13 T2) — `SET DEFAULT
11292                        // nextval('…')` is how pg_dump spells a
11293                        // SERIAL column (plain integer in CREATE
11294                        // TABLE + this ALTER). It used to be
11295                        // swallowed as a no-op, which silently
11296                        // STRIPPED auto-increment from imported
11297                        // schemas — the first post-import INSERT
11298                        // without an explicit id then violated NOT
11299                        // NULL. Lower it to the auto-increment
11300                        // marker instead.
11301                        let is_default_nextval =
11302                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11303                                && matches!(
11304                                    self.tokens.get(self.pos + 2),
11305                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11306                                );
11307                        if is_default_nextval {
11308                            let seq_name = self.scan_sequence_name_until_boundary();
11309                            return Ok(alloc::vec![
11310                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11311                                    column: col_name,
11312                                    seq_name,
11313                                }
11314                            ]);
11315                        }
11316                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11317                        self.advance(); // consume "set"
11318                        match self.peek().clone() {
11319                            Token::Default => {
11320                                self.advance();
11321                                let default_expr = self.parse_expr(0)?;
11322                                return Ok(alloc::vec![
11323                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11324                                        column: col_name,
11325                                        default_expr,
11326                                    }
11327                                ]);
11328                            }
11329                            Token::Not => {
11330                                self.advance();
11331                                if !matches!(self.peek(), Token::Null) {
11332                                    return Err(self.err(alloc::format!(
11333                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11334                                        self.peek()
11335                                    )));
11336                                }
11337                                self.advance();
11338                                return Ok(alloc::vec![
11339                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11340                                        column: col_name,
11341                                    }
11342                                ]);
11343                            }
11344                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11345                            // stored generated column's expression and
11346                            // recompute existing rows.
11347                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11348                                self.advance(); // EXPRESSION
11349                                if matches!(self.peek(), Token::As) {
11350                                    self.advance();
11351                                }
11352                                let expr = self.parse_expr(0)?;
11353                                return Ok(alloc::vec![
11354                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11355                                        column: col_name,
11356                                        expr,
11357                                    }
11358                                ]);
11359                            }
11360                            other => {
11361                                // Other SET subjects (STATISTICS,
11362                                // STORAGE, COMPRESSION, …) stay no-ops —
11363                                // storage hints with no SPG semantics.
11364                                let _ = other;
11365                                self.consume_until_statement_boundary();
11366                                return Ok(Vec::new());
11367                            }
11368                        }
11369                    }
11370                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11371                        self.advance(); // consume "drop"
11372                        return self.parse_alter_column_drop_tail(col_name);
11373                    }
11374                    Token::Drop => {
11375                        self.advance(); // consume Drop token
11376                        return self.parse_alter_column_drop_tail(col_name);
11377                    }
11378                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11379                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11380                        // GENERATED { ALWAYS | BY DEFAULT } AS
11381                        // IDENTITY ( … )`: pg_dump's spelling for
11382                        // identity columns. Same auto-increment
11383                        // lowering as the nextval default; the
11384                        // sequence options inside the parens are
11385                        // no-ops under SPG's max+1 semantics.
11386                        let is_generated = matches!(
11387                            self.tokens.get(self.pos + 1),
11388                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11389                        );
11390                        if !is_generated {
11391                            return Err(self.err(alloc::format!(
11392                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11393                                self.tokens.get(self.pos + 1)
11394                            )));
11395                        }
11396                        let seq_name = self.scan_sequence_name_until_boundary();
11397                        return Ok(alloc::vec![
11398                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11399                                column: col_name,
11400                                seq_name,
11401                            }
11402                        ]);
11403                    }
11404                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11405                    // column: floor the next allocated value at n (bare
11406                    // RESTART = restart from the start value, 1).
11407                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11408                        self.advance();
11409                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11410                        {
11411                            self.advance();
11412                            let neg = if matches!(self.peek(), Token::Minus) {
11413                                self.advance();
11414                                true
11415                            } else {
11416                                false
11417                            };
11418                            match self.advance() {
11419                                Token::Integer(v) => Some(if neg { -v } else { v }),
11420                                other => {
11421                                    return Err(self.err(alloc::format!(
11422                                        "expected integer after RESTART WITH, got {other:?}"
11423                                    )));
11424                                }
11425                            }
11426                        } else {
11427                            None
11428                        };
11429                        return Ok(alloc::vec![
11430                            crate::ast::AlterTableTarget::AlterColumnRestart {
11431                                column: col_name,
11432                                with,
11433                            }
11434                        ]);
11435                    }
11436                    other => {
11437                        return Err(self.err(alloc::format!(
11438                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11439                        )));
11440                    }
11441                }
11442                // v7.39 (round 713) — the type parser has consumed a
11443                // trailing `COLLATE <name>` since Phase 2.5, and
11444                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11445                // TYPE text COLLATE "C"` parsed clean and changed
11446                // nothing. Keep the clause; the engine re-collates.
11447                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11448                    self.parse_type_with_implied_flags()?;
11449                let collation = if coll_explicit {
11450                    coll_name.map(|n| (coll, n))
11451                } else {
11452                    None
11453                };
11454                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11455                {
11456                    self.advance();
11457                    Some(self.parse_expr(0)?)
11458                } else {
11459                    None
11460                };
11461                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11462                    column: col_name,
11463                    new_type,
11464                    using,
11465                    collation,
11466                }])
11467            }
11468            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11469            // PG also supports `RENAME TO new_table` for table-name
11470            // rename; that surface is deferred (pg_dump never emits
11471            // it). If the first post-RENAME ident is `TO`, the user
11472            // is asking for table rename — error with a clear
11473            // message rather than misparsing `TO` as a column name.
11474            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11475                self.advance();
11476                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11477                // table-name rename (mailrs round-10 A.5 — used
11478                // by migrate-042's `RENAME TO email_contacts`).
11479                // `TO` lexes as Token::To.
11480                if matches!(self.peek(), Token::To)
11481                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11482                {
11483                    self.advance();
11484                    let new = self.expect_ident_like()?;
11485                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11486                        new,
11487                    }]);
11488                }
11489                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11490                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11491                    self.advance();
11492                    let old = self.expect_ident_like()?;
11493                    if matches!(self.peek(), Token::To) {
11494                        self.advance();
11495                    } else {
11496                        self.expect_keyword_ident("to")?;
11497                    }
11498                    let new = self.expect_ident_like()?;
11499                    return Ok(alloc::vec![
11500                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11501                    ]);
11502                }
11503                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11504                    self.advance();
11505                }
11506                let old = self.expect_ident_like()?;
11507                // `TO` is a reserved keyword token; accept both
11508                // Token::To and Token::Ident("to") for consistency.
11509                if matches!(self.peek(), Token::To) {
11510                    self.advance();
11511                } else {
11512                    self.expect_keyword_ident("to")?;
11513                }
11514                let new = self.expect_ident_like()?;
11515                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11516                    old,
11517                    new,
11518                }])
11519            }
11520            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11521            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11522            // every data block with these. Real disable semantics —
11523            // not no-op — because reload correctness assumes the
11524            // triggers don't fire (rows already carry their
11525            // computed values from prod).
11526            Token::Ident(s)
11527                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11528            {
11529                let enabled = s.eq_ignore_ascii_case("enable");
11530                self.advance();
11531                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11532                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11533                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11534                // pg_dump output) — anything else falls through to
11535                // the catch-all error below.
11536                // v7.22 (round-13 T3) — mysqldump wraps every data
11537                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11538                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11539                // maintains indexes incrementally — engine no-op.
11540                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11541                    self.advance();
11542                    return Ok(Vec::new());
11543                }
11544                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11545                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11546                // to gate triggers on session_replication_role; SPG
11547                // has no replica role, so the prefix is consumed and
11548                // treated identically to the plain ENABLE/DISABLE
11549                // TRIGGER form.
11550                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11551                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11552                {
11553                    self.advance();
11554                }
11555                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11556                    return Err(self.err(alloc::format!(
11557                        "expected TRIGGER after {}, got {:?}",
11558                        if enabled { "ENABLE" } else { "DISABLE" },
11559                        self.peek()
11560                    )));
11561                }
11562                self.advance();
11563                // `ALL` lexes as Token::All (reserved); also
11564                // accept Token::Ident("all") for symmetry.
11565                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11566                // TRIGGER selectors. USER (= all user triggers) is
11567                // semantically ALL here; REPLICA / ALWAYS gate on
11568                // session_replication_role which SPG doesn't track.
11569                // All map to TriggerSelector::All.
11570                let which = if matches!(self.peek(), Token::All)
11571                    || matches!(self.peek(), Token::Ident(s)
11572                        if s.eq_ignore_ascii_case("all")
11573                            || s.eq_ignore_ascii_case("user")
11574                            || s.eq_ignore_ascii_case("replica")
11575                            || s.eq_ignore_ascii_case("always"))
11576                {
11577                    self.advance();
11578                    crate::ast::TriggerSelector::All
11579                } else {
11580                    let name = self.expect_ident_like()?;
11581                    crate::ast::TriggerSelector::Named(name)
11582                };
11583                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11584                    which,
11585                    enabled,
11586                }])
11587            }
11588            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11589            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11590                self.advance();
11591                if !matches!(self.peek(), Token::Partition)
11592                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11593                        if s.eq_ignore_ascii_case("partition"))
11594                {
11595                    return Err(self.err(alloc::format!(
11596                        "expected PARTITION after ATTACH, got {:?}",
11597                        self.peek()
11598                    )));
11599                }
11600                self.advance();
11601                let child = self.expect_ident_like()?;
11602                let bounds = self.parse_partition_bounds_tail()?;
11603                Ok(alloc::vec![
11604                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11605                ])
11606            }
11607            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11608            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11609                self.advance();
11610                if !matches!(self.peek(), Token::Partition)
11611                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11612                        if s.eq_ignore_ascii_case("partition"))
11613                {
11614                    return Err(self.err(alloc::format!(
11615                        "expected PARTITION after DETACH, got {:?}",
11616                        self.peek()
11617                    )));
11618                }
11619                self.advance();
11620                let child = self.expect_ident_like()?;
11621                let mut concurrently = false;
11622                let mut finalize = false;
11623                loop {
11624                    match self.peek().clone() {
11625                        Token::Ident(s) | Token::QuotedIdent(s)
11626                            if s.eq_ignore_ascii_case("concurrently") =>
11627                        {
11628                            self.advance();
11629                            concurrently = true;
11630                        }
11631                        Token::Ident(s) | Token::QuotedIdent(s)
11632                            if s.eq_ignore_ascii_case("finalize") =>
11633                        {
11634                            self.advance();
11635                            finalize = true;
11636                        }
11637                        _ => break,
11638                    }
11639                }
11640                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11641                    child,
11642                    concurrently,
11643                    finalize,
11644                }])
11645            }
11646            other => Err(self.err(alloc::format!(
11647                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11648            ))),
11649        }
11650    }
11651
11652    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11653    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11654    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11655    /// `parse_partition_of_tail`'s bounds branch.
11656    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11657    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11658    /// lowering each to the respective AlterTableTarget. Any
11659    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11660    /// no-op via consume_until_statement_boundary.
11661    fn parse_alter_column_drop_tail(
11662        &mut self,
11663        col_name: String,
11664    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11665        match self.peek().clone() {
11666            Token::Default => {
11667                self.advance();
11668                Ok(alloc::vec![
11669                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11670                ])
11671            }
11672            Token::Not => {
11673                self.advance();
11674                if !matches!(self.peek(), Token::Null) {
11675                    return Err(self.err(alloc::format!(
11676                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11677                        self.peek()
11678                    )));
11679                }
11680                self.advance();
11681                Ok(alloc::vec![
11682                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11683                ])
11684            }
11685            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11686            // generated column into a plain column.
11687            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11688                self.advance();
11689                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11690                // dropped, so the engine still errored on a plain
11691                // column; PG's semantics are NOTICE + skip.
11692                let mut if_exists = false;
11693                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11694                    self.advance();
11695                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11696                        self.advance();
11697                        if_exists = true;
11698                    }
11699                }
11700                Ok(alloc::vec![
11701                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11702                        column: col_name,
11703                        if_exists,
11704                    }
11705                ])
11706            }
11707            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11708            // identity column into a plain column.
11709            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11710                self.advance();
11711                let mut if_exists = false;
11712                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11713                    self.advance();
11714                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11715                        self.advance();
11716                        if_exists = true;
11717                    }
11718                }
11719                Ok(alloc::vec![
11720                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11721                        column: col_name,
11722                        if_exists,
11723                    }
11724                ])
11725            }
11726            _ => {
11727                self.consume_until_statement_boundary();
11728                Ok(Vec::new())
11729            }
11730        }
11731    }
11732
11733    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11734    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11735    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11736    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11737    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11738        let mut opts = crate::ast::CopyOptions::default();
11739        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11740            return Ok(opts);
11741        }
11742        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11743            self.advance();
11744        }
11745        if matches!(self.peek(), Token::LParen) {
11746            self.advance();
11747            loop {
11748                self.parse_one_copy_option(&mut opts)?;
11749                match self.peek() {
11750                    Token::Comma => {
11751                        self.advance();
11752                    }
11753                    Token::RParen => {
11754                        self.advance();
11755                        break;
11756                    }
11757                    other => {
11758                        return Err(self.err(alloc::format!(
11759                            "expected ',' or ')' in COPY options, got {other:?}"
11760                        )));
11761                    }
11762                }
11763            }
11764        } else {
11765            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11766                self.parse_one_copy_option(&mut opts)?;
11767            }
11768        }
11769        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11770            return Err(self.err(alloc::format!(
11771                "unexpected token after COPY options: {:?}",
11772                self.peek()
11773            )));
11774        }
11775        Ok(opts)
11776    }
11777
11778    fn parse_one_copy_option(
11779        &mut self,
11780        opts: &mut crate::ast::CopyOptions,
11781    ) -> Result<(), ParseError> {
11782        use crate::ast::CopyFormat;
11783        // The option keyword. NULL lexes as its own token; the rest are
11784        // bare identifiers.
11785        let kw = match self.advance() {
11786            Token::Null => alloc::string::String::from("NULL"),
11787            Token::Ident(s) => s.to_uppercase(),
11788            other => {
11789                return Err(self.err(alloc::format!(
11790                    "expected a COPY option keyword, got {other:?}"
11791                )));
11792            }
11793        };
11794        match kw.as_str() {
11795            "FORMAT" => {
11796                let fmt = self.expect_ident_like()?;
11797                match fmt.to_ascii_uppercase().as_str() {
11798                    "CSV" => opts.format = CopyFormat::Csv,
11799                    "TEXT" => opts.format = CopyFormat::Text,
11800                    other => {
11801                        return Err(self.err(alloc::format!(
11802                            "COPY format \"{}\" not recognized",
11803                            other.to_ascii_lowercase()
11804                        )));
11805                    }
11806                }
11807            }
11808            // Legacy bare format keywords.
11809            "CSV" => opts.format = CopyFormat::Csv,
11810            "TEXT" => opts.format = CopyFormat::Text,
11811            "HEADER" => {
11812                opts.header = match self.peek() {
11813                    Token::True => {
11814                        self.advance();
11815                        true
11816                    }
11817                    Token::False => {
11818                        self.advance();
11819                        false
11820                    }
11821                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11822                        self.advance();
11823                        true
11824                    }
11825                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11826                        self.advance();
11827                        false
11828                    }
11829                    // Bare HEADER (no boolean) means HEADER true.
11830                    _ => true,
11831                };
11832            }
11833            // r1066 (7.38 S5.1) — pgbench 14+ loads with
11834            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11835            // vacuum bookkeeping on a freshly created/truncated
11836            // table; SPG's per-statement visibility makes it a
11837            // faithful no-op, and rejecting it aborted `pgbench -i`
11838            // against the drop-in. Accept ON/OFF/bare, change nothing.
11839            "FREEZE" => match self.peek() {
11840                Token::True | Token::False => {
11841                    self.advance();
11842                }
11843                Token::Ident(s)
11844                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11845                {
11846                    self.advance();
11847                }
11848                _ => {}
11849            },
11850            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11851                let s = match self.advance() {
11852                    Token::String(s) => s,
11853                    other => {
11854                        return Err(self.err(alloc::format!(
11855                            "COPY {kw} expects a single-character string, got {other:?}"
11856                        )));
11857                    }
11858                };
11859                // v7.39 (round 247) — PG's wording (0A000), keyword in
11860                // lowercase: "COPY delimiter must be a single one-byte
11861                // character".
11862                let one_byte_err = || {
11863                    self.err(alloc::format!(
11864                        "COPY {} must be a single one-byte character",
11865                        kw.to_ascii_lowercase()
11866                    ))
11867                };
11868                let mut chars = s.chars();
11869                let c = chars.next().ok_or_else(one_byte_err)?;
11870                if chars.next().is_some() || c.len_utf8() != 1 {
11871                    return Err(one_byte_err());
11872                }
11873                match kw.as_str() {
11874                    "DELIMITER" => opts.delimiter = Some(c),
11875                    "QUOTE" => opts.quote = Some(c),
11876                    _ => opts.escape = Some(c),
11877                }
11878            }
11879            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11880            "FORCE_QUOTE" => {
11881                if matches!(self.peek(), Token::Star) {
11882                    self.advance();
11883                    opts.force_quote = Some(Vec::new());
11884                } else {
11885                    if !matches!(self.peek(), Token::LParen) {
11886                        return Err(self.err(alloc::format!(
11887                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11888                            self.peek()
11889                        )));
11890                    }
11891                    self.advance();
11892                    let mut cols = Vec::new();
11893                    loop {
11894                        cols.push(self.expect_ident_like()?);
11895                        match self.peek() {
11896                            Token::Comma => {
11897                                self.advance();
11898                            }
11899                            Token::RParen => {
11900                                self.advance();
11901                                break;
11902                            }
11903                            other => {
11904                                return Err(self.err(alloc::format!(
11905                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11906                                )));
11907                            }
11908                        }
11909                    }
11910                    opts.force_quote = Some(cols);
11911                }
11912            }
11913            "NULL" => {
11914                opts.null_str = Some(match self.advance() {
11915                    Token::String(s) => s,
11916                    other => {
11917                        return Err(self.err(alloc::format!(
11918                            "COPY NULL expects a quoted string, got {other:?}"
11919                        )));
11920                    }
11921                });
11922            }
11923            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11924            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11925            // FORCE_NULL too.
11926            "FORCE_NOT_NULL" | "FORCE_NULL" => {
11927                let cols = self.parse_copy_column_list(&kw)?;
11928                if kw == "FORCE_NOT_NULL" {
11929                    opts.force_not_null = Some(cols);
11930                } else {
11931                    opts.force_null = Some(cols);
11932                }
11933            }
11934            other => {
11935                // PG's wording, lowercased option name.
11936                return Err(self.err(alloc::format!(
11937                    "option \"{}\" not recognized",
11938                    other.to_ascii_lowercase()
11939                )));
11940            }
11941        }
11942        Ok(())
11943    }
11944
11945    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11946    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11947    /// is the `*` spelling.
11948    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11949        if matches!(self.peek(), Token::Star) {
11950            self.advance();
11951            return Ok(Vec::new());
11952        }
11953        if !matches!(self.peek(), Token::LParen) {
11954            return Err(self.err(alloc::format!(
11955                "expected '(' or '*' after {kw}, got {:?}",
11956                self.peek()
11957            )));
11958        }
11959        self.advance();
11960        let mut cols = Vec::new();
11961        loop {
11962            cols.push(self.expect_ident_like()?);
11963            match self.peek() {
11964                Token::Comma => {
11965                    self.advance();
11966                }
11967                Token::RParen => {
11968                    self.advance();
11969                    break;
11970                }
11971                other => {
11972                    return Err(self.err(alloc::format!(
11973                        "expected ',' or ')' in {kw} list, got {other:?}"
11974                    )));
11975                }
11976            }
11977        }
11978        Ok(cols)
11979    }
11980
11981    fn parse_partition_bounds_tail(
11982        &mut self,
11983    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11984        use crate::ast::PartitionOfBoundsAst;
11985        match self.peek() {
11986            Token::Default => {
11987                self.advance();
11988                Ok(PartitionOfBoundsAst::Default)
11989            }
11990            Token::For => {
11991                self.advance();
11992                if !matches!(self.peek(), Token::Values) {
11993                    return Err(
11994                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11995                    );
11996                }
11997                self.advance();
11998                let want_with = matches!(
11999                    self.peek(),
12000                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12001                );
12002                if want_with {
12003                    self.advance();
12004                    if !matches!(self.peek(), Token::LParen) {
12005                        return Err(self.err(format!(
12006                            "expected '(' after FOR VALUES WITH, got {:?}",
12007                            self.peek()
12008                        )));
12009                    }
12010                    self.advance();
12011                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12012                    loop {
12013                        let key = self.expect_ident_like()?;
12014                        let n = match self.peek().clone() {
12015                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12016                                self.advance();
12017                                v as u32
12018                            }
12019                            other => {
12020                                return Err(self.err(format!(
12021                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12022                                )));
12023                            }
12024                        };
12025                        match key.to_ascii_uppercase().as_str() {
12026                            "MODULUS" => modulus = Some(n),
12027                            "REMAINDER" => remainder = Some(n),
12028                            other => {
12029                                return Err(self.err(format!(
12030                                    "FOR VALUES WITH: unknown key {other:?}; \
12031                                     expected MODULUS or REMAINDER"
12032                                )));
12033                            }
12034                        }
12035                        match self.peek() {
12036                            Token::Comma => {
12037                                self.advance();
12038                            }
12039                            Token::RParen => {
12040                                self.advance();
12041                                break;
12042                            }
12043                            other => {
12044                                return Err(self.err(format!(
12045                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12046                                )));
12047                            }
12048                        }
12049                    }
12050                    let modulus = modulus
12051                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12052                    let remainder = remainder.ok_or_else(|| {
12053                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12054                    })?;
12055                    if modulus == 0 {
12056                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12057                    }
12058                    if remainder >= modulus {
12059                        return Err(self.err(format!(
12060                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12061                        )));
12062                    }
12063                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12064                }
12065                match self.peek() {
12066                    Token::From => {
12067                        self.advance();
12068                        let lower = Box::new(self.parse_partition_bound_expr()?);
12069                        if !matches!(self.peek(), Token::To) {
12070                            return Err(self.err(format!(
12071                                "expected TO after FROM (...), got {:?}",
12072                                self.peek()
12073                            )));
12074                        }
12075                        self.advance();
12076                        let upper = Box::new(self.parse_partition_bound_expr()?);
12077                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12078                    }
12079                    Token::In => {
12080                        self.advance();
12081                        if !matches!(self.peek(), Token::LParen) {
12082                            return Err(self.err(format!(
12083                                "expected '(' after FOR VALUES IN, got {:?}",
12084                                self.peek()
12085                            )));
12086                        }
12087                        self.advance();
12088                        let mut values = Vec::new();
12089                        loop {
12090                            values.push(self.parse_expr(0)?);
12091                            match self.peek() {
12092                                Token::Comma => {
12093                                    self.advance();
12094                                }
12095                                Token::RParen => {
12096                                    self.advance();
12097                                    break;
12098                                }
12099                                other => {
12100                                    return Err(self.err(format!(
12101                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12102                                    )));
12103                                }
12104                            }
12105                        }
12106                        if values.is_empty() {
12107                            return Err(
12108                                self.err("FOR VALUES IN requires at least one literal".to_string())
12109                            );
12110                        }
12111                        Ok(PartitionOfBoundsAst::List { values })
12112                    }
12113                    other => Err(self.err(format!(
12114                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12115                    ))),
12116                }
12117            }
12118            other => Err(self.err(format!(
12119                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12120            ))),
12121        }
12122    }
12123
12124    /// v7.16.2 — peek for `information_schema.<tbl>` /
12125    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12126    /// three tokens + return a synthetic table name the engine's
12127    /// SELECT path recognises as a virtual view. Returns `None`
12128    /// when the head doesn't look like a meta-qualified name.
12129    /// Used by `parse_table_ref` to bypass the
12130    /// `expect_ident_like` schema-strip for these specific PG
12131    /// meta schemas (mailrs round-10 A.3).
12132    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12133        // Extract the schema name. Must be a plain ident token.
12134        let schema = match self.tokens.get(self.pos) {
12135            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12136            _ => return None,
12137        };
12138        // Dot.
12139        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12140            return None;
12141        }
12142        // The table-side ident may lex as a reserved keyword
12143        // (e.g. `Token::Tables`). Tolerate the common ones via a
12144        // helper that reads the trailing token's underlying name.
12145        let tbl = match self.tokens.get(self.pos + 2)? {
12146            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12147            Token::Tables => "tables".to_string(),
12148            // Other PG meta table names that may collide with
12149            // reserved keywords land here as needed.
12150            _ => return None,
12151        };
12152        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12153        // names so the synthetic name doesn't double-prefix
12154        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12155        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12156            ("__spg_info_", tbl.to_ascii_lowercase())
12157        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12158            // v7.39 (round 541) — only the catalogs SPG actually
12159            // synthesises are rewritten, which is what the BARE path
12160            // has always checked. Anything else keeps its own name and
12161            // takes the ordinary route: `pg_stat_activity` and friends
12162            // resolve through meta_view_result, and a name that is no
12163            // catalog at all gets PG's "relation does not exist"
12164            // instead of a message about a view SPG cannot materialise.
12165            let lowered = tbl.to_ascii_lowercase();
12166            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12167                self.advance(); // schema
12168                self.advance(); // dot
12169                self.advance(); // tbl
12170                return Some((lowered.clone(), lowered));
12171            }
12172            let bare = lowered
12173                .strip_prefix("pg_")
12174                .map(alloc::string::String::from)
12175                .unwrap_or(lowered);
12176            ("__spg_pg_", bare)
12177        } else if schema.eq_ignore_ascii_case("mysql") {
12178            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12179            // (`mysql.user`, `mysql.db`). Same synthetic-name
12180            // shape as pg_catalog.
12181            ("__spg_mysql_", tbl.to_ascii_lowercase())
12182        } else {
12183            return None;
12184        };
12185        self.advance(); // schema
12186        self.advance(); // dot
12187        self.advance(); // tbl
12188        Some((
12189            alloc::format!("{prefix}{normalised}"),
12190            tbl.to_ascii_lowercase(),
12191        ))
12192    }
12193
12194    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12195    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12196    /// implicit front of every search_path, so a bare reference to a
12197    /// known catalog table always means the catalog table. Only the
12198    /// names the engine actually synthesises are recognised — any
12199    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12200    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12201        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12202        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12203        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12204        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12205        // through the meta_view_result path instead, and already resolve
12206        // bare — they must NOT be listed here or the __spg_ rewrite would
12207        // mis-target them.)
12208        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12209        let name = match self.tokens.get(self.pos) {
12210            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12211            _ => return None,
12212        };
12213        // A following dot means this ident is a schema qualifier,
12214        // not a table name — let the qualified path handle it.
12215        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12216            return None;
12217        }
12218        if !PG_META_TABLES.contains(&name.as_str()) {
12219            return None;
12220        }
12221        self.advance();
12222        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12223        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12224    }
12225
12226    /// Consume a bare ident if its lowercase matches `kw`, else err.
12227    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12228    /// Peeks only; the caller advances.
12229    fn peek_keyword_ident(&self, kw: &str) -> bool {
12230        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12231    }
12232
12233    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12234        match self.advance() {
12235            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12236            other => Err(ParseError {
12237                message: format!("expected {kw:?}, got {other:?}"),
12238                token_pos: self.consumed_pos(),
12239            }),
12240        }
12241    }
12242
12243    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12244    /// literal (`'foo'`) — same shape used by CREATE USER for the
12245    /// username slot.
12246    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12247        match self.advance() {
12248            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12249            other => Err(ParseError {
12250                message: format!("expected identifier or string, got {other:?}"),
12251                token_pos: self.consumed_pos(),
12252            }),
12253        }
12254    }
12255
12256    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12257        match self.advance() {
12258            Token::String(s) => Ok(s),
12259            other => Err(ParseError {
12260                message: format!("expected quoted string, got {other:?}"),
12261                token_pos: self.consumed_pos(),
12262            }),
12263        }
12264    }
12265
12266    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12267        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12268        // subqueries recurse through here without passing
12269        // parse_expr; share the same nesting budget.
12270        self.enter_nested()?;
12271        let r = self.parse_select_stmt_inner();
12272        self.nest_depth -= 1;
12273        r
12274    }
12275
12276    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12277        // Caller dispatches on Token::Select; the inner helper handles
12278        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12279        // get a fresh bare-select parse and may not have their own ORDER
12280        // BY / LIMIT.
12281        let mut head = self.parse_bare_select()?;
12282        self.parse_setop_chain_into(&mut head)?;
12283        self.parse_select_tail_into(&mut head)?;
12284        Ok(Statement::Select(head))
12285    }
12286
12287    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12288    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12289    /// token), and INTERSECT [ALL] (a bare ident — it was never
12290    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12291    /// tighter than UNION / EXCEPT — the executor folds the chain
12292    /// left-to-right, which is already correct for LEADING
12293    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12294    /// pair nests into that previous peer, so A UNION B INTERSECT C
12295    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12296    /// groups.
12297    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12298        // A parenthesized group arrives with its own (already
12299        // regrouped) unions on `head`; only the pairs THIS chain
12300        // appends participate in the precedence regroup below —
12301        // nesting an outer INTERSECT into a group-internal peer
12302        // would dissolve the explicit grouping.
12303        let boundary = head.unions.len();
12304        loop {
12305            let base = match self.peek() {
12306                Token::Union => UnionKind::Distinct,
12307                Token::Except => UnionKind::Except,
12308                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12309                _ => break,
12310            };
12311            self.advance();
12312            let kind = if matches!(self.peek(), Token::All) {
12313                self.advance();
12314                match base {
12315                    UnionKind::Distinct => UnionKind::All,
12316                    UnionKind::Except => UnionKind::ExceptAll,
12317                    _ => UnionKind::IntersectAll,
12318                }
12319            } else {
12320                base
12321            };
12322            let peer = self.parse_bare_select()?;
12323            head.unions.push((kind, peer));
12324        }
12325        let mut pairs = core::mem::take(&mut head.unions);
12326        let tail = pairs.split_off(boundary);
12327        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12328        for (kind, peer) in tail {
12329            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12330            // An intersect nests into the previous element of THIS
12331            // chain only; with no new previous element it stays at
12332            // the outer level (the left fold applies it to the
12333            // whole head, group included).
12334            match (
12335                is_intersect,
12336                regrouped.len() > boundary,
12337                regrouped.last_mut(),
12338            ) {
12339                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12340                _ => regrouped.push((kind, peer)),
12341            }
12342        }
12343        head.unions = regrouped;
12344        Ok(())
12345    }
12346
12347    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12348    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12349    /// the top-level bare VALUES statement reuses it verbatim.
12350    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12351    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12352    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12353    /// where the grouping-set universe is still in scope.
12354    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12355        if !matches!(self.peek(), Token::Order) {
12356            return Ok(Vec::new());
12357        }
12358        self.advance();
12359        if !self.peek_is_by() {
12360            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12361        }
12362        self.advance();
12363        let mut keys = Vec::new();
12364        loop {
12365            // v7.39 (round 691) — save/restore, the discipline this parser
12366            // already uses around `pending_sample_preds`, so a subquery inside
12367            // a key neither inherits nor leaks the channel.
12368            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12369            let saved_coll = self.order_key_collation.take();
12370            let parsed = self.parse_expr(0);
12371            self.in_order_by_key = saved_flag;
12372            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12373            let expr = parsed?;
12374            let desc = if matches!(self.peek(), Token::Desc) {
12375                self.advance();
12376                true
12377            } else if matches!(self.peek(), Token::Asc) {
12378                self.advance();
12379                false
12380            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12381                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12382                // one ordering per type, so the btree comparison operators map
12383                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12384                // would need a custom operator class — honest error.
12385                self.advance();
12386                match self.advance() {
12387                    Token::Lt | Token::LtEq => false,
12388                    Token::Gt | Token::GtEq => true,
12389                    other => {
12390                        return Err(self.err(alloc::format!(
12391                            "ORDER BY USING supports the btree comparison \
12392                             operators (< <= > >=); got {other:?}"
12393                        )));
12394                    }
12395                }
12396            } else {
12397                false
12398            };
12399            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12400            let nulls_first = self.parse_optional_nulls_placement()?;
12401            keys.push(OrderBy {
12402                expr,
12403                desc,
12404                nulls_first,
12405                collation,
12406            });
12407            if matches!(self.peek(), Token::Comma) {
12408                self.advance();
12409            } else {
12410                break;
12411            }
12412        }
12413        Ok(keys)
12414    }
12415
12416    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12417        // v7.39 (round 135) — a grouping-set query may have already parsed +
12418        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12419        // no ORDER BY token is present, keep that pre-set order_by rather than
12420        // clobbering it with an empty list.
12421        let parsed_keys = self.parse_order_by_keys()?;
12422        head.order_by = if parsed_keys.is_empty() {
12423            core::mem::take(&mut head.order_by)
12424        } else {
12425            parsed_keys
12426        };
12427        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12428        // order. PG's grammar takes a limit clause and an offset clause
12429        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12430        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12431        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12432        // spelling died on `expected end of input, got Limit`.
12433        //
12434        // Each may appear at most once, and LIMIT and FETCH FIRST are
12435        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12436        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12437        // A second one is left unconsumed here, which the caller reports
12438        // as trailing input rather than silently taking the last.
12439        let mut saw_limit = false;
12440        let mut saw_offset = false;
12441        loop {
12442            if !saw_limit && matches!(self.peek(), Token::Limit) {
12443                self.advance();
12444                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12445                // PG synonyms for "no limit". Treat both as None
12446                // (no head.limit set) so the engine's existing
12447                // unlimited-result path takes over. Reject was the
12448                // pre-5.1 behaviour and broke pg_dump-flavoured
12449                // tooling that occasionally emits LIMIT NULL.
12450                if self.consume_limit_unbounded_sentinel() {
12451                    head.limit = None;
12452                } else {
12453                    let first = self.parse_limit_expr("LIMIT")?;
12454                    // MySQL `LIMIT offset, count` — the first number is
12455                    // the offset when a comma follows.
12456                    if matches!(self.peek(), Token::Comma) {
12457                        self.advance();
12458                        let count = self.parse_limit_expr("LIMIT")?;
12459                        head.offset = Some(first);
12460                        saw_offset = true;
12461                        head.limit = Some(count);
12462                    } else {
12463                        head.limit = Some(first);
12464                    }
12465                }
12466                saw_limit = true;
12467                continue;
12468            }
12469            if !saw_offset && matches!(self.peek(), Token::Offset) {
12470                self.advance();
12471                // PG also accepts an optional `ROW` / `ROWS` trailer
12472                // after the offset value (`OFFSET 10 ROWS`). The
12473                // FETCH-FIRST branch below relies on the same.
12474                let off = self.parse_limit_expr("OFFSET")?;
12475                self.consume_optional_rows_keyword();
12476                head.offset = Some(off);
12477                saw_offset = true;
12478                continue;
12479            }
12480            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12481            // the SQL-standard alias for LIMIT. PG accepts both
12482            // spellings interchangeably; pg_dump emits FETCH FIRST in
12483            // newer versions. We map it onto `head.limit` so the
12484            // engine path is unified.
12485            if !saw_limit
12486                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12487                    if s.eq_ignore_ascii_case("fetch"))
12488            {
12489                self.advance(); // FETCH
12490                // `FIRST` or `NEXT` (both legal per SQL standard).
12491                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12492                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12493                {
12494                    self.advance();
12495                }
12496                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12497                // implicit 1 — but we always consume one if present).
12498                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12499                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12500                {
12501                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12502                    crate::ast::LimitExpr::Literal(1)
12503                } else {
12504                    self.parse_limit_expr("FETCH FIRST")?
12505                };
12506                // Eat `ROW` / `ROWS` if not already consumed above.
12507                self.consume_optional_rows_keyword();
12508                // Optional `ONLY` (the spec form) — or the SQL:2008
12509                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12510                // now honours WITH TIES by extending past the LIMIT
12511                // truncation point through every row that shares the
12512                // last-kept row's ORDER BY key.
12513                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12514                    if s.eq_ignore_ascii_case("only"))
12515                {
12516                    self.advance();
12517                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12518                    if s.eq_ignore_ascii_case("with"))
12519                {
12520                    self.advance(); // WITH
12521                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12522                        if s.eq_ignore_ascii_case("ties"))
12523                    {
12524                        self.advance();
12525                        head.limit_with_ties = true;
12526                    }
12527                }
12528                head.limit = Some(count);
12529                saw_limit = true;
12530                continue;
12531            }
12532            break;
12533        }
12534        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12535        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12536        //       [ OF table_name [, …] ]
12537        //       [ NOWAIT | SKIP LOCKED ]
12538        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12539        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12540        // SELECT already returns a consistent snapshot — so these
12541        // are accept-and-discard: the parser absorbs them so
12542        // mailrs / Rails / Django code paths that emit `SELECT
12543        // … FOR UPDATE` for advisory pessimistic locking load
12544        // without a parser error. The on-disk locking model is
12545        // unchanged; callers that rely on FOR UPDATE for read-
12546        // through-write ordering still get the right answer
12547        // because SPG serialises writes anyway.
12548        head.locking = self
12549            .consume_optional_for_lock_clauses()
12550            .map(alloc::boxed::Box::new);
12551        Ok(())
12552    }
12553
12554    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12555    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12556    /// LOCKED ]` trailers. Each clause is fully accepted and
12557    /// discarded — SPG's single-writer model already satisfies the
12558    /// callers' implicit ordering requirement. Stops at the first
12559    /// token that isn't `FOR`.
12560    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12561        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12562        // not discarded. PG keeps the strongest of several clauses; the
12563        // policy of the last one wins, which is what this loop records.
12564        let mut seen: Option<crate::ast::LockingClause> = None;
12565        while matches!(self.peek(), Token::For) {
12566            // v7.37.14 (A2.5-stub) — record that this query asked
12567            // for a row lock the parser is about to silently
12568            // discard. Operators surface the count via
12569            // `spg_sql::silent_for_update_count()` so they can
12570            // gauge how much of the workload depends on advisory
12571            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12572            // before v7.37.15's per-row tuple locking lands.
12573            crate::record_silent_for_update_clause();
12574            self.advance(); // FOR
12575            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12576            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12577            let mut no_key = false;
12578            let mut key = false;
12579            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12580                if s.eq_ignore_ascii_case("no"))
12581            {
12582                self.advance(); // NO
12583                no_key = true;
12584                // The next ident should be KEY but be generous;
12585                // anything followed by UPDATE/SHARE is accepted.
12586                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12587                    if s.eq_ignore_ascii_case("key"))
12588                {
12589                    self.advance(); // KEY
12590                }
12591            }
12592            // `KEY` prefix (PG `FOR KEY SHARE`).
12593            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12594                if s.eq_ignore_ascii_case("key"))
12595            {
12596                self.advance(); // KEY
12597                key = true;
12598            }
12599            // Lock-strength keyword: UPDATE / SHARE. Required, but
12600            // we're lenient — an unexpected token here just bails
12601            // (we already consumed FOR; caller's downstream
12602            // dispatch will error if anything actually depends on
12603            // the trailing tokens).
12604            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12605                if s.eq_ignore_ascii_case("update"));
12606            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12607                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12608            {
12609                self.advance();
12610                use crate::ast::LockStrength as LS;
12611                let strength = match (is_update, no_key, key) {
12612                    (true, true, _) => LS::NoKeyUpdate,
12613                    (true, _, _) => LS::Update,
12614                    (false, _, true) => LS::KeyShare,
12615                    (false, _, _) => LS::Share,
12616                };
12617                seen = Some(crate::ast::LockingClause {
12618                    strength,
12619                    of_tables: alloc::vec::Vec::new(),
12620                    policy: crate::ast::LockWait::Wait,
12621                });
12622            } else {
12623                // FOR by itself (or `FOR KEY` with nothing after) —
12624                // give up on the lock-clause path. We've already
12625                // advanced past FOR; further attempts to parse
12626                // here would clobber state.
12627                return seen;
12628            }
12629            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12630            // joining and locking only a subset of tables.
12631            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12632                if s.eq_ignore_ascii_case("of"))
12633            {
12634                self.advance(); // OF
12635                #[allow(clippy::while_let_loop)]
12636                loop {
12637                    match self.peek() {
12638                        Token::Ident(_) | Token::QuotedIdent(_) => {
12639                            // v7.39 (round 294) — the name is CAPTURED now: PG
12640                            // validates it against the FROM clause, and an
12641                            // uncaptured list silently means "lock everything".
12642                            let mut nm = match self.advance() {
12643                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12644                                _ => alloc::string::String::new(),
12645                            };
12646                            // Optional schema-qualified `schema.table`.
12647                            if matches!(self.peek(), Token::Dot) {
12648                                self.advance();
12649                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12650                                {
12651                                    self.advance();
12652                                    nm = n;
12653                                }
12654                            }
12655                            if let Some(c) = seen.as_mut() {
12656                                c.of_tables.push(nm);
12657                            }
12658                        }
12659                        _ => break,
12660                    }
12661                    if matches!(self.peek(), Token::Comma) {
12662                        self.advance();
12663                    } else {
12664                        break;
12665                    }
12666                }
12667            }
12668            // Optional `NOWAIT` | `SKIP LOCKED`.
12669            match self.peek().clone() {
12670                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12671                    self.advance();
12672                    if let Some(c) = seen.as_mut() {
12673                        c.policy = crate::ast::LockWait::NoWait;
12674                    }
12675                }
12676                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12677                    self.advance(); // SKIP
12678                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12679                        if s.eq_ignore_ascii_case("locked"))
12680                    {
12681                        self.advance(); // LOCKED
12682                        if let Some(c) = seen.as_mut() {
12683                            c.policy = crate::ast::LockWait::SkipLocked;
12684                        }
12685                    }
12686                }
12687                _ => {}
12688            }
12689            // Loop: PG allows multiple FOR clauses chained.
12690        }
12691        seen
12692    }
12693
12694    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12695    /// Bind value gets resolved during prepared-statement Execute;
12696    /// the Pratt expression parser would over-accept here (e.g.
12697    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12698    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12699    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12700    /// when one was consumed; caller skips the regular
12701    /// limit-value parse and leaves `head.limit` at None.
12702    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12703        if matches!(self.peek(), Token::Null) {
12704            self.advance();
12705            return true;
12706        }
12707        if matches!(self.peek(), Token::All) {
12708            self.advance();
12709            return true;
12710        }
12711        false
12712    }
12713
12714    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12715    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12716    /// SQL-standard shape. No-op when missing.
12717    fn consume_optional_rows_keyword(&mut self) {
12718        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12719            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12720        {
12721            self.advance();
12722        }
12723    }
12724
12725    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12726    ///
12727    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12728    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12729    /// constant, which is why that spelling keeps the token path below.
12730    ///
12731    /// Constants are folded here rather than carried into the tree: the
12732    /// 15+ execution paths that read the row count go through
12733    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12734    /// means "no limit". A clause the engine could not resolve would
12735    /// therefore return the WHOLE table instead of failing. Folding at
12736    /// parse time keeps that impossible; a non-constant clause is still
12737    /// a clean error (recorded residual — closing it wants a resolution
12738    /// pre-pass on the simple-query path, where `substitute_placeholders`
12739    /// does not run).
12740    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12741        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12742        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12743        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12744        // ONLY` both work (its grammar takes a c_expr). Both measured
12745        // against PG 18.4 in round 305.
12746        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12747            return self.parse_limit_constant(label);
12748        }
12749        // One pass, no rewind: `advance()` takes each token by
12750        // `mem::replace`, so a consumed token reads back as Eof and this
12751        // parser cannot backtrack. Everything — bare literal included —
12752        // is therefore folded from the parsed expression rather than
12753        // re-read from the token stream.
12754        let start = self.pos;
12755        let e = self.parse_expr(0)?;
12756        if let crate::ast::Expr::Placeholder(n) = e {
12757            return Ok(crate::ast::LimitExpr::Placeholder(n));
12758        }
12759        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12760        match fold_limit_constant(&e) {
12761            Some(Ok(v)) if v < 0 => Err(ParseError {
12762                message: alloc::format!("{neg_label} must not be negative"),
12763                token_pos: start,
12764            }),
12765            Some(Ok(v)) => u32::try_from(v)
12766                .map(crate::ast::LimitExpr::Literal)
12767                .map_err(|_| ParseError {
12768                    message: alloc::format!("{label} value too large: {v}"),
12769                    token_pos: start,
12770                }),
12771            Some(Err(message)) => Err(ParseError {
12772                message: message.replace("{L}", neg_label),
12773                token_pos: start,
12774            }),
12775            // v7.39 (round 305, V23) — not foldable at parse time
12776            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12777            // expression; the engine evaluates it once before dispatch.
12778            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12779        }
12780    }
12781
12782    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12783        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12784        // coercion rules, not just an integer token: a NUMERIC rounds half
12785        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12786        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12787        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12788        // content, failing as an input-syntax error on the value. General
12789        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12790        // they need an Expr-carrying LimitExpr variant.
12791        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12792        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12793            message,
12794            token_pos: pos,
12795        };
12796        match self.advance() {
12797            Token::Integer(n) if n >= 0 => u32::try_from(n)
12798                .map(crate::ast::LimitExpr::Literal)
12799                .map_err(|_| ParseError {
12800                    message: alloc::format!("{label} value too large: {n}"),
12801                    token_pos: self.consumed_pos(),
12802                }),
12803            Token::Integer(_) => Err(err_at(
12804                alloc::format!("{neg_label} must not be negative"),
12805                self.pos.saturating_sub(1),
12806            )),
12807            Token::Numeric(t) => {
12808                let pos = self.pos.saturating_sub(1);
12809                let v: f64 = t.parse().map_err(|_| {
12810                    err_at(
12811                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12812                        pos,
12813                    )
12814                })?;
12815                if v < 0.0 {
12816                    return Err(err_at(
12817                        alloc::format!("{neg_label} must not be negative"),
12818                        pos,
12819                    ));
12820                }
12821                // Round half away from zero — PG's numeric→bigint cast.
12822                // (no_std: no f64::round; v is non-negative, so truncating
12823                // v + 0.5 is the same thing.)
12824                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12825                let rounded = (v + 0.5) as u64;
12826                u32::try_from(rounded)
12827                    .map(crate::ast::LimitExpr::Literal)
12828                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12829            }
12830            Token::Minus => {
12831                let pos = self.pos.saturating_sub(1);
12832                match self.peek() {
12833                    Token::Integer(_) | Token::Numeric(_) => {
12834                        self.advance();
12835                        Err(err_at(
12836                            alloc::format!("{neg_label} must not be negative"),
12837                            pos,
12838                        ))
12839                    }
12840                    other => Err(err_at(
12841                        alloc::format!(
12842                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12843                        ),
12844                        pos,
12845                    )),
12846                }
12847            }
12848            Token::String(t) => {
12849                let pos = self.pos.saturating_sub(1);
12850                match t.trim().parse::<i64>() {
12851                    Ok(n) if n < 0 => Err(err_at(
12852                        alloc::format!("{neg_label} must not be negative"),
12853                        pos,
12854                    )),
12855                    Ok(n) => u32::try_from(n)
12856                        .map(crate::ast::LimitExpr::Literal)
12857                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12858                    Err(_) => Err(err_at(
12859                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12860                        pos,
12861                    )),
12862                }
12863            }
12864            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12865            other => Err(ParseError {
12866                message: alloc::format!(
12867                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12868                ),
12869                token_pos: self.consumed_pos(),
12870            }),
12871        }
12872    }
12873
12874    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12875    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12876    /// `unions` empty and `order_by` / `limit` `None`; the top-level
12877    /// `parse_select_stmt` is responsible for filling those in.
12878    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12879    /// call in the expression tree to the per-set integer bitmask
12880    /// (PG semantics: one bit per argument, MSB first; 1 = the key
12881    /// is dropped in this grouping set). Runs during the ROLLUP /
12882    /// CUBE / GROUPING SETS expansion, where the set is known.
12883    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12884    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12885    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12886        if let Expr::FunctionCall { name, .. } = expr
12887            && name.eq_ignore_ascii_case("grouping")
12888        {
12889            if !out.iter().any(|e| e == expr) {
12890                out.push(expr.clone());
12891            }
12892            return;
12893        }
12894        match expr {
12895            Expr::Binary { lhs, rhs, .. } => {
12896                Self::collect_grouping_calls(lhs, out);
12897                Self::collect_grouping_calls(rhs, out);
12898            }
12899            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12900                Self::collect_grouping_calls(expr, out)
12901            }
12902            Expr::FunctionCall { args, .. } => {
12903                for a in args {
12904                    Self::collect_grouping_calls(a, out);
12905                }
12906            }
12907            Expr::Case {
12908                operand,
12909                branches,
12910                else_branch,
12911            } => {
12912                if let Some(o) = operand {
12913                    Self::collect_grouping_calls(o, out);
12914                }
12915                for (c, v) in branches {
12916                    Self::collect_grouping_calls(c, out);
12917                    Self::collect_grouping_calls(v, out);
12918                }
12919                if let Some(x) = else_branch {
12920                    Self::collect_grouping_calls(x, out);
12921                }
12922            }
12923            _ => {}
12924        }
12925    }
12926
12927    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12928    /// `grp_exprs[k]` with a reference to the synthetic ordering column
12929    /// `__grp_ord_k` (injected per grouping-set branch).
12930    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12931        if let Expr::FunctionCall { name, .. } = expr
12932            && name.eq_ignore_ascii_case("grouping")
12933        {
12934            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12935                *expr = Expr::Column(crate::ast::ColumnName {
12936                    qualifier: None,
12937                    name: alloc::format!("__grp_ord_{k}"),
12938                });
12939            }
12940            return;
12941        }
12942        match expr {
12943            Expr::Binary { lhs, rhs, .. } => {
12944                Self::rewrite_grouping_to_col(lhs, grp_exprs);
12945                Self::rewrite_grouping_to_col(rhs, grp_exprs);
12946            }
12947            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12948                Self::rewrite_grouping_to_col(expr, grp_exprs)
12949            }
12950            Expr::FunctionCall { args, .. } => {
12951                for a in args {
12952                    Self::rewrite_grouping_to_col(a, grp_exprs);
12953                }
12954            }
12955            Expr::Case {
12956                operand,
12957                branches,
12958                else_branch,
12959            } => {
12960                if let Some(o) = operand {
12961                    Self::rewrite_grouping_to_col(o, grp_exprs);
12962                }
12963                for (c, v) in branches {
12964                    Self::rewrite_grouping_to_col(c, grp_exprs);
12965                    Self::rewrite_grouping_to_col(v, grp_exprs);
12966                }
12967                if let Some(x) = else_branch {
12968                    Self::rewrite_grouping_to_col(x, grp_exprs);
12969                }
12970            }
12971            _ => {}
12972        }
12973    }
12974
12975    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12976    /// as the list of key sets it contributes. A bare expression is one
12977    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12978    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12979    /// the concatenation of its items' sets, where an item is itself an
12980    /// element, a parenthesized key list, or the empty set `()`. A
12981    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12982    /// move together.
12983    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12984        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12985        // ROLLUP ( … ) / CUBE ( … )
12986        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12987            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12988        {
12989            let is_cube = is_kw(self.peek(), "cube");
12990            self.advance(); // ROLLUP / CUBE
12991            self.advance(); // (
12992            let mut units: Vec<Vec<Expr>> = Vec::new();
12993            loop {
12994                if matches!(self.peek(), Token::LParen) {
12995                    // Composite unit: (a, b) rolls up as one.
12996                    self.advance();
12997                    let mut unit = Vec::new();
12998                    if !matches!(self.peek(), Token::RParen) {
12999                        loop {
13000                            unit.push(self.parse_expr(0)?);
13001                            match self.peek() {
13002                                Token::Comma => {
13003                                    self.advance();
13004                                }
13005                                Token::RParen => break,
13006                                other => {
13007                                    return Err(self.err(format!(
13008                                        "expected ',' or ')' in grouping unit, got {other:?}"
13009                                    )));
13010                                }
13011                            }
13012                        }
13013                    }
13014                    self.advance(); // )
13015                    units.push(unit);
13016                } else {
13017                    units.push(alloc::vec![self.parse_expr(0)?]);
13018                }
13019                match self.peek() {
13020                    Token::Comma => {
13021                        self.advance();
13022                    }
13023                    Token::RParen => break,
13024                    other => {
13025                        return Err(self.err(format!(
13026                            "expected ',' or ')' in grouping list, got {other:?}"
13027                        )));
13028                    }
13029                }
13030            }
13031            self.advance(); // )
13032            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13033                units
13034                    .iter()
13035                    .zip(unit_sel.iter())
13036                    .filter(|(_, keep)| **keep)
13037                    .flat_map(|(u, _)| u.iter().cloned())
13038                    .collect()
13039            };
13040            let n = units.len();
13041            if is_cube {
13042                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13043                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13044                    .collect();
13045                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13046                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13047            }
13048            return Ok((0..=n)
13049                .rev()
13050                .map(|keep| {
13051                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13052                    flatten(&sel)
13053                })
13054                .collect());
13055        }
13056        // GROUPING SETS ( item [, item]* )
13057        if is_kw(self.peek(), "grouping")
13058            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13059        {
13060            self.advance(); // GROUPING
13061            self.advance(); // SETS
13062            if !matches!(self.peek(), Token::LParen) {
13063                return Err(self.err(format!(
13064                    "expected '(' after GROUPING SETS, got {:?}",
13065                    self.peek()
13066                )));
13067            }
13068            self.advance(); // outer (
13069            let mut sets: Vec<Vec<Expr>> = Vec::new();
13070            loop {
13071                if matches!(self.peek(), Token::LParen) {
13072                    // A parenthesized key list (or the empty set).
13073                    self.advance();
13074                    let mut set = Vec::new();
13075                    if !matches!(self.peek(), Token::RParen) {
13076                        loop {
13077                            set.push(self.parse_expr(0)?);
13078                            match self.peek() {
13079                                Token::Comma => {
13080                                    self.advance();
13081                                }
13082                                Token::RParen => break,
13083                                other => {
13084                                    return Err(self.err(format!(
13085                                        "expected ',' or ')' in grouping set, got {other:?}"
13086                                    )));
13087                                }
13088                            }
13089                        }
13090                    }
13091                    self.advance(); // )
13092                    sets.push(set);
13093                } else {
13094                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13095                    // bare expression.
13096                    sets.extend(self.parse_grouping_element()?);
13097                }
13098                match self.peek() {
13099                    Token::Comma => {
13100                        self.advance();
13101                    }
13102                    Token::RParen => break,
13103                    other => {
13104                        return Err(self.err(format!(
13105                            "expected ',' or ')' after a grouping set, got {other:?}"
13106                        )));
13107                    }
13108                }
13109            }
13110            self.advance(); // outer )
13111            return Ok(sets);
13112        }
13113        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13114    }
13115
13116    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13117        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13118        // set evaluates to NULL, at any depth. Previously only a *top-level*
13119        // select item equal to a dropped key was nullified, so a key nested in
13120        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13121        // column and failed to resolve against the set's synthetic schema.
13122        if dropped.iter().any(|d| d == expr) {
13123            *expr = Expr::Literal(Literal::Null);
13124            return;
13125        }
13126        if let Expr::FunctionCall { name, args } = expr
13127            && name.eq_ignore_ascii_case("grouping")
13128        {
13129            let mut mask: i64 = 0;
13130            for a in args.iter() {
13131                mask <<= 1;
13132                if dropped.iter().any(|d| d == a) {
13133                    mask |= 1;
13134                }
13135            }
13136            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13137            // literal: a bare integer in a select item is indistinguishable
13138            // from a positional reference once `ORDER BY 1` substitutes the
13139            // item back in, and the round-232 position check then read the
13140            // mask value as an out-of-range position. The cast changes
13141            // nothing semantically (grouping() is integer).
13142            *expr = Expr::Cast {
13143                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13144                target: crate::ast::CastTarget::Int,
13145            };
13146            return;
13147        }
13148        // Generic recursion over the common expression shapes the
13149        // SELECT list uses; anything without child expressions is
13150        // left alone.
13151        match expr {
13152            Expr::FunctionCall { args, .. } => {
13153                for a in args {
13154                    Self::substitute_grouping_calls(a, dropped);
13155                }
13156            }
13157            Expr::Binary { lhs, rhs, .. } => {
13158                Self::substitute_grouping_calls(lhs, dropped);
13159                Self::substitute_grouping_calls(rhs, dropped);
13160            }
13161            Expr::Unary { expr: inner, .. } => {
13162                Self::substitute_grouping_calls(inner, dropped);
13163            }
13164            Expr::Cast { expr: inner, .. } => {
13165                Self::substitute_grouping_calls(inner, dropped);
13166            }
13167            Expr::Case {
13168                operand,
13169                branches,
13170                else_branch,
13171            } => {
13172                if let Some(op) = operand {
13173                    Self::substitute_grouping_calls(op, dropped);
13174                }
13175                for (w, t) in branches {
13176                    Self::substitute_grouping_calls(w, dropped);
13177                    Self::substitute_grouping_calls(t, dropped);
13178                }
13179                if let Some(e) = else_branch {
13180                    Self::substitute_grouping_calls(e, dropped);
13181                }
13182            }
13183            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13184            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13185            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13186            // …` is the canonical rollup-total label idiom).
13187            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13188            Expr::Like { expr, pattern, .. } => {
13189                Self::substitute_grouping_calls(expr, dropped);
13190                Self::substitute_grouping_calls(pattern, dropped);
13191            }
13192            Expr::InList { expr, list, .. } => {
13193                Self::substitute_grouping_calls(expr, dropped);
13194                for item in list {
13195                    Self::substitute_grouping_calls(item, dropped);
13196                }
13197            }
13198            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13199            Expr::Array(items) => {
13200                for item in items {
13201                    Self::substitute_grouping_calls(item, dropped);
13202                }
13203            }
13204            Expr::ArraySubscript { target, index } => {
13205                Self::substitute_grouping_calls(target, dropped);
13206                Self::substitute_grouping_calls(index, dropped);
13207            }
13208            Expr::ArraySlice { target, lo, hi } => {
13209                Self::substitute_grouping_calls(target, dropped);
13210                if let Some(lo) = lo {
13211                    Self::substitute_grouping_calls(lo, dropped);
13212                }
13213                if let Some(hi) = hi {
13214                    Self::substitute_grouping_calls(hi, dropped);
13215                }
13216            }
13217            Expr::AnyAll { expr, array, .. } => {
13218                Self::substitute_grouping_calls(expr, dropped);
13219                Self::substitute_grouping_calls(array, dropped);
13220            }
13221            _ => {}
13222        }
13223    }
13224
13225    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13226        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13227        // group: `( <select chain> )` usable anywhere a query block
13228        // is (head or peer of an outer chain). The group's own
13229        // unions ride the returned SelectStatement; the executor's
13230        // nested-peer recursion runs them.
13231        if matches!(self.peek(), Token::LParen)
13232            && matches!(
13233                self.tokens.get(self.pos + 1),
13234                Some(Token::Select | Token::LParen | Token::Values)
13235            )
13236        {
13237            self.advance(); // (
13238            self.enter_nested()?;
13239            // v7.37 D.20 — a group whose head is a VALUES list:
13240            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13241            // otherwise recurse into a nested SELECT/group head.
13242            let mut head = (if matches!(self.peek(), Token::Values) {
13243                self.advance(); // VALUES
13244                self.parse_values_rows_body()
13245            } else {
13246                self.parse_bare_select()
13247            })
13248            .and_then(|mut h| {
13249                self.parse_setop_chain_into(&mut h)?;
13250                Ok(h)
13251            });
13252            self.nest_depth -= 1;
13253            let mut head = match &mut head {
13254                Ok(h) => core::mem::take(h),
13255                Err(_) => return head,
13256            };
13257            // v7.37.17 (17.6 siblings) — group-internal tail:
13258            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13259            // group head, then wrap the group as a derived table
13260            // (SELECT * FROM (group)) so the outer chain / outer
13261            // tail can't clobber the group's own ordering or limit.
13262            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13263                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13264                    if s.eq_ignore_ascii_case("fetch"));
13265            if has_tail {
13266                self.parse_select_tail_into(&mut head)?;
13267                head = SelectStatement {
13268                    locking: None,
13269                    ctes: Vec::new(),
13270                    distinct: false,
13271                    distinct_on: Vec::new(),
13272                    items: alloc::vec![SelectItem::Wildcard],
13273                    from: Some(FromClause {
13274                        primary: TableRef {
13275                            name: "subquery".to_string(),
13276                            alias: None,
13277                            only: false,
13278                            as_of_segment: None,
13279                            unnest_expr: None,
13280                            unnest_column_aliases: Vec::new(),
13281                            with_ordinality: false,
13282                            generate_series_args: None,
13283                            lateral_subquery: Some(Box::new(head)),
13284                            jsonb_each_text_arg: None,
13285                            table_fn_call: None,
13286                            rows_from: None,
13287                            json_table: None,
13288                            scalar_fn_item: false,
13289                        },
13290                        joins: Vec::new(),
13291                    }),
13292                    where_: None,
13293                    group_by: None,
13294                    group_by_all: false,
13295                    having: None,
13296                    unions: Vec::new(),
13297                    order_by: Vec::new(),
13298                    limit: None,
13299                    offset: None,
13300                    limit_with_ties: false,
13301                    window_check_exprs: Vec::new(),
13302                };
13303            }
13304            if !matches!(self.peek(), Token::RParen) {
13305                return Err(self.err(format!(
13306                    "expected ')' after parenthesized query group, got {:?}",
13307                    self.peek()
13308                )));
13309            }
13310            self.advance();
13311            return Ok(head);
13312        }
13313        // `TABLE name` shorthand as a query block — valid anywhere
13314        // a SELECT head is (set-op peers included).
13315        if matches!(self.peek(), Token::Table)
13316            && matches!(
13317                self.tokens.get(self.pos + 1),
13318                Some(Token::Ident(_) | Token::QuotedIdent(_))
13319            )
13320        {
13321            return self.parse_table_shorthand();
13322        }
13323        if !matches!(self.peek(), Token::Select) {
13324            return Err(self.err(format!(
13325                "expected SELECT to start a query block, got {:?}",
13326                self.peek()
13327            )));
13328        }
13329        self.advance();
13330        let distinct = if matches!(self.peek(), Token::Distinct) {
13331            self.advance();
13332            true
13333        } else {
13334            false
13335        };
13336        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13337        // keep the first row (per ORDER BY) of each group the
13338        // expressions define. Django's .distinct('field') shape.
13339        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13340            self.advance(); // ON
13341            if !matches!(self.peek(), Token::LParen) {
13342                return Err(self.err(format!(
13343                    "expected '(' after DISTINCT ON, got {:?}",
13344                    self.peek()
13345                )));
13346            }
13347            self.advance();
13348            let mut exprs = Vec::new();
13349            loop {
13350                exprs.push(self.parse_expr(0)?);
13351                match self.peek() {
13352                    Token::Comma => {
13353                        self.advance();
13354                    }
13355                    Token::RParen => break,
13356                    other => {
13357                        return Err(self.err(format!(
13358                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13359                        )));
13360                    }
13361                }
13362            }
13363            self.advance(); // )
13364            exprs
13365        } else {
13366            Vec::new()
13367        };
13368        let mut items = self.parse_select_list()?;
13369        // Scope the TABLESAMPLE lowering channel to this SELECT:
13370        // stash whatever an enclosing select accumulated, collect
13371        // our own FROM's predicates, restore after the combine.
13372        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13373        let mut from = if matches!(self.peek(), Token::From) {
13374            self.advance();
13375            Some(self.parse_from_clause()?)
13376        } else {
13377            None
13378        };
13379        // v7.37 D.22 — a set-returning function in the projection with no FROM
13380        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13381        // rows. Move the first SRF projection item to a FROM-position derived
13382        // table and replace it in the projection with a reference to its output
13383        // column; sibling scalar columns repeat per SRF row. PG names the output
13384        // column after the function (or its AS alias). Reuses the FROM-SRF
13385        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13386        // works via the targetlist-SRF path.
13387        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13388        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13389        // is exactly what the function's own row shape already is. Anywhere else
13390        // (per outer row, or beside other items) it would need a real record-typed
13391        // projection, so it says so rather than answering something else.
13392        if let [
13393            SelectItem::Expr {
13394                expr: Expr::FunctionCall { name, args },
13395                ..
13396            },
13397        ] = items.as_slice()
13398            && name == "__record_expand"
13399        {
13400            let Some(Expr::FunctionCall {
13401                name: inner_name,
13402                args: inner_args,
13403            }) = args.first()
13404            else {
13405                return Err(self.err(
13406                    "(<expr>).* expands a function's record — it needs a function call".into(),
13407                ));
13408            };
13409            if from.is_some() {
13410                return Err(self.err(
13411                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13412                        .into(),
13413                ));
13414            }
13415            let fn_ref = TableRef {
13416                name: inner_name.clone(),
13417                alias: None,
13418                only: false,
13419                as_of_segment: None,
13420                unnest_expr: None,
13421                unnest_column_aliases: Vec::new(),
13422                with_ordinality: false,
13423                generate_series_args: None,
13424                lateral_subquery: None,
13425                jsonb_each_text_arg: None,
13426                table_fn_call: Some(Box::new((
13427                    inner_name.to_ascii_lowercase(),
13428                    inner_args.clone(),
13429                ))),
13430                rows_from: None,
13431                json_table: None,
13432                scalar_fn_item: false,
13433            };
13434            items = alloc::vec![SelectItem::Wildcard];
13435            from = Some(FromClause {
13436                primary: fn_ref,
13437                joins: Vec::new(),
13438            });
13439        }
13440        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13441        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13442        // record's fields takes the catalog. It becomes a LATERAL of the same
13443        // function plus one item per declared column — the machinery rounds 65
13444        // and 69 already built.
13445        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13446        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13447        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13448        // express, since the lifted one becomes a scan and the other would
13449        // expand per its rows (a cross product, not a zip). So when the
13450        // projection holds more than one top-level function call, the lift steps
13451        // aside and the engine's target-list expansion takes the whole list.
13452        let fn_call_items = items
13453            .iter()
13454            .filter(|it| {
13455                matches!(
13456                    it,
13457                    SelectItem::Expr {
13458                        expr: Expr::FunctionCall { .. },
13459                        ..
13460                    }
13461                )
13462            })
13463            .count();
13464        if from.is_none() && fn_call_items <= 1 {
13465            let mut found: Option<(usize, TableRef, String)> = None;
13466            for (i, item) in items.iter().enumerate() {
13467                if let SelectItem::Expr {
13468                    expr: Expr::FunctionCall { name, args },
13469                    alias,
13470                } = item
13471                {
13472                    let lname = name.to_ascii_lowercase();
13473                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13474                    let (unnest, gs) = match lname.as_str() {
13475                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13476                        "generate_series" if (2..=3).contains(&args.len()) => {
13477                            (None, Some(args.clone()))
13478                        }
13479                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13480                        // no-FROM projection yields the 1-based subscripts, i.e.
13481                        // generate_series(1, array_length(arr, dim)); an invalid
13482                        // dimension makes array_length NULL → 0 rows, as in PG.
13483                        "generate_subscripts" if args.len() == 2 => (
13484                            None,
13485                            Some(alloc::vec![
13486                                Expr::Literal(Literal::Integer(1)),
13487                                Expr::FunctionCall {
13488                                    name: "array_length".to_string(),
13489                                    args: args.clone(),
13490                                },
13491                            ]),
13492                        ),
13493                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13494                        // in a no-FROM projection unnest their *_to_array form.
13495                        "string_to_table" | "regexp_split_to_table" => {
13496                            let array_fn = if lname == "string_to_table" {
13497                                "string_to_array"
13498                            } else {
13499                                "regexp_split_to_array"
13500                            };
13501                            (
13502                                Some(Box::new(Expr::FunctionCall {
13503                                    name: array_fn.to_string(),
13504                                    args: args.clone(),
13505                                })),
13506                                None,
13507                            )
13508                        }
13509                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13510                        // a no-FROM projection expand per element. The scalar form
13511                        // returns the elements as a TEXT array, so unnest over the
13512                        // same call materialises one row each (same rewrite the
13513                        // FROM-clause form uses).
13514                        "jsonb_array_elements"
13515                        | "json_array_elements"
13516                        | "jsonb_array_elements_text"
13517                        | "json_array_elements_text"
13518                            if args.len() == 1 =>
13519                        {
13520                            (
13521                                Some(Box::new(Expr::FunctionCall {
13522                                    name: lname.clone(),
13523                                    args: args.clone(),
13524                                })),
13525                                None,
13526                            )
13527                        }
13528                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13529                        // in a no-FROM projection expands per match (scalar form
13530                        // returns the matches as a TEXT array → unnest).
13531                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13532                            Some(Box::new(Expr::FunctionCall {
13533                                name: lname.clone(),
13534                                args: args.clone(),
13535                            })),
13536                            None,
13537                        ),
13538                        _ => continue,
13539                    };
13540                    found = Some((
13541                        i,
13542                        TableRef {
13543                            name: colname.clone(),
13544                            alias: Some(colname.clone()),
13545                            only: false,
13546                            as_of_segment: None,
13547                            unnest_expr: unnest,
13548                            unnest_column_aliases: alloc::vec![colname.clone()],
13549                            with_ordinality: false,
13550                            generate_series_args: gs,
13551                            lateral_subquery: None,
13552                            jsonb_each_text_arg: None,
13553                            table_fn_call: None,
13554                            rows_from: None,
13555                            json_table: None,
13556                            scalar_fn_item: false,
13557                        },
13558                        colname,
13559                    ));
13560                    break;
13561                }
13562            }
13563            if let Some((idx, tref, colname)) = found {
13564                from = Some(FromClause {
13565                    primary: tref,
13566                    joins: Vec::new(),
13567                });
13568                items[idx] = SelectItem::Expr {
13569                    expr: Expr::Column(ColumnName {
13570                        qualifier: None,
13571                        name: colname.clone(),
13572                    }),
13573                    alias: Some(colname),
13574                };
13575            }
13576        }
13577        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13578        let where_ = if matches!(self.peek(), Token::Where) {
13579            self.advance();
13580            Some(self.parse_expr(0)?)
13581        } else {
13582            None
13583        };
13584        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13585            Some(match acc {
13586                Some(w) => Expr::Binary {
13587                    lhs: Box::new(pred),
13588                    op: crate::ast::BinOp::And,
13589                    rhs: Box::new(w),
13590                },
13591                None => pred,
13592            })
13593        });
13594        self.pending_sample_preds = enclosing_sample_preds;
13595        let mut group_by_all = false;
13596        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13597        // share one expansion: `grouping_sets` lists the key subsets
13598        // (first = primary, assigned to stmt.group_by; the rest
13599        // become UNION ALL peers), `grouping_universe` is the full
13600        // key list used to compute each peer's dropped keys.
13601        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13602        let mut grouping_universe: Vec<Expr> = Vec::new();
13603        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13604        // A BOOL, not the key list: this frame is the statement parser's, and
13605        // round 430 measured that a `Vec` local here is enough on its own to
13606        // tip the 512 KiB nesting guard. The keys are recoverable from
13607        // `grouping_universe`, which a rollup fills with exactly them.
13608        let mut mysql_rollup = false;
13609        let group_by = if matches!(self.peek(), Token::Group) {
13610            self.advance();
13611            if !self.peek_is_by() {
13612                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13613            }
13614            self.advance();
13615            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13616            // every non-aggregate SELECT-list item later.
13617            if matches!(self.peek(), Token::All) {
13618                self.advance();
13619                group_by_all = true;
13620                None
13621            } else {
13622                // v7.39 (round 242) — PG's general grouping-element grammar:
13623                // GROUP BY [DISTINCT] element [, element]*, where an element
13624                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13625                // SETS (…) — mixed freely. Each element yields a list of
13626                // key sets; the query's grouping sets are the CARTESIAN
13627                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13628                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13629                // content. ROLLUP/CUBE members may be composite
13630                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13631                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13632                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13633                // clause.
13634                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13635                    self.advance();
13636                    true
13637                } else {
13638                    false
13639                };
13640                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13641                loop {
13642                    element_sets.push(self.parse_grouping_element()?);
13643                    if matches!(self.peek(), Token::Comma) {
13644                        self.advance();
13645                    } else {
13646                        break;
13647                    }
13648                }
13649                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13650                for el in &element_sets {
13651                    let mut next: Vec<Vec<Expr>> = Vec::new();
13652                    for base in &total {
13653                        for set in el {
13654                            let mut merged = base.clone();
13655                            for k in set {
13656                                if !merged.iter().any(|m| m == k) {
13657                                    merged.push(k.clone());
13658                                }
13659                            }
13660                            next.push(merged);
13661                        }
13662                    }
13663                    total = next;
13664                }
13665                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13666                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13667                // The keys and the aggregates come out identical; the ROW
13668                // ORDER does not, and that is the part a report depends on.
13669                // MySQL interleaves each group's subtotal right after its
13670                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13671                // where the union-of-grouping-sets expansion emits every
13672                // leaf first and then every subtotal. MariaDB REFUSES an
13673                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13674                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13675                // agree on the order and disagree only on whether ORDER BY
13676                // is allowed (MySQL allows it; SPG allows it too, since
13677                // refusing would break the clients that can write it).
13678                if self.mysql_dialect
13679                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13680                    && matches!(
13681                        self.tokens.get(self.pos + 1),
13682                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13683                    )
13684                {
13685                    self.advance(); // WITH
13686                    self.advance(); // ROLLUP
13687                    let keys = total.into_iter().next().unwrap_or_default();
13688                    mysql_rollup = true;
13689                    // n+1 prefixes, largest first — the same expansion
13690                    // `ROLLUP (…)` produces.
13691                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13692                }
13693                if distinct_sets {
13694                    let mut seen: Vec<Vec<String>> = Vec::new();
13695                    total.retain(|set| {
13696                        let mut key: Vec<String> =
13697                            set.iter().map(|e| alloc::format!("{e}")).collect();
13698                        key.sort();
13699                        if seen.contains(&key) {
13700                            false
13701                        } else {
13702                            seen.push(key);
13703                            true
13704                        }
13705                    });
13706                }
13707                if total.len() > 1 {
13708                    let mut universe: Vec<Expr> = Vec::new();
13709                    for set in &total {
13710                        for k in set {
13711                            if !universe.iter().any(|u| u == k) {
13712                                universe.push(k.clone());
13713                            }
13714                        }
13715                    }
13716                    grouping_universe = universe;
13717                    let primary = total[0].clone();
13718                    grouping_sets = total;
13719                    Some(primary)
13720                } else {
13721                    // One set (a plain GROUP BY list, or a single-set
13722                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13723                    // single set — GROUPING SETS (()) — stays
13724                    // `Some(vec![])`: the grand-total group, which must
13725                    // run the aggregate path.
13726                    Some(total.into_iter().next().unwrap_or_default())
13727                }
13728            }
13729        } else {
13730            None
13731        };
13732        let having = if matches!(self.peek(), Token::Having) {
13733            self.advance();
13734            Some(self.parse_expr(0)?)
13735        } else {
13736            None
13737        };
13738        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13739        // OVER w parsed to a marker above; inline each definition
13740        // into the referencing WindowFunction nodes.
13741        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13742        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13743            self.advance();
13744            loop {
13745                let wname = self.expect_ident_like()?;
13746                if !matches!(self.peek(), Token::As) {
13747                    return Err(self.err(format!(
13748                        "expected AS after WINDOW {wname}, got {:?}",
13749                        self.peek()
13750                    )));
13751                }
13752                self.advance();
13753                // v7.39 (round 229) — PG rejects a redefinition outright.
13754                if window_defs
13755                    .iter()
13756                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13757                {
13758                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13759                }
13760                let def = self.parse_over_clause()?;
13761                // A definition may itself copy an earlier one
13762                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13763                // so resolve it against the defs already in scope. Same
13764                // copy rules as an `OVER (w1 …)` in the select list.
13765                let mut probe = Expr::WindowFunction {
13766                    name: String::new(),
13767                    args: Vec::new(),
13768                    partition_by: def.0,
13769                    order_by: def.1,
13770                    frame: def.2,
13771                    null_treatment: crate::ast::NullTreatment::Respect,
13772                    filter: None,
13773                };
13774                Self::substitute_named_windows(&mut probe, &window_defs)
13775                    .map_err(|m| self.err(m))?;
13776                let Expr::WindowFunction {
13777                    partition_by,
13778                    order_by,
13779                    frame,
13780                    ..
13781                } = probe
13782                else {
13783                    unreachable!("probe is a WindowFunction")
13784                };
13785                window_defs.push((wname, (partition_by, order_by, frame)));
13786                if matches!(self.peek(), Token::Comma) {
13787                    self.advance();
13788                    continue;
13789                }
13790                break;
13791            }
13792        }
13793        // v7.39 (round 705) — which definitions did anything reference?
13794        // The ones nothing did used to be dropped here, unexamined, so
13795        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13796        // definition whether referenced or not. Their key expressions ride
13797        // out on the statement for the engine to resolve.
13798        let mut window_refs: Vec<String> = Vec::new();
13799        if !window_defs.is_empty() {
13800            for it in &items {
13801                if let SelectItem::Expr { expr, .. } = it {
13802                    Self::collect_named_window_refs(expr, &mut window_refs);
13803                }
13804            }
13805        }
13806        let window_check_exprs: Vec<Expr> = window_defs
13807            .iter()
13808            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13809            .flat_map(|(_, (partition, order, _))| {
13810                partition
13811                    .iter()
13812                    .cloned()
13813                    .chain(order.iter().map(|(e, _, _)| e.clone()))
13814            })
13815            .collect();
13816        if !window_defs.is_empty()
13817            || items
13818                .iter()
13819                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13820        {
13821            for it in &mut items {
13822                if let SelectItem::Expr { expr, .. } = it {
13823                    Self::substitute_named_windows(expr, &window_defs)
13824                        .map_err(|m| self.err(m))?;
13825                }
13826            }
13827        }
13828        // `GROUP BY 1` — positional keys substitute with the Nth
13829        // select item's expression (same contract ORDER BY has had
13830        // since v6.x). Out-of-range positions error.
13831        let group_by = match group_by {
13832            Some(mut keys) => {
13833                for k in &mut keys {
13834                    if let Expr::Literal(Literal::Integer(n)) = k {
13835                        let idx = *n;
13836                        if idx < 1 || idx as usize > items.len() {
13837                            return Err(self.err(alloc::format!(
13838                                "GROUP BY position {idx} is not in select list"
13839                            )));
13840                        }
13841                        match &items[(idx - 1) as usize] {
13842                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
13843                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13844                                return Err(self.err(alloc::format!(
13845                                    "GROUP BY position {idx} references a wildcard item"
13846                                )));
13847                            }
13848                        }
13849                    }
13850                }
13851                Some(keys)
13852            }
13853            None => None,
13854        };
13855        let mut stmt = SelectStatement {
13856            locking: None,
13857            ctes: Vec::new(),
13858            distinct,
13859            distinct_on,
13860            items,
13861            from,
13862            where_,
13863            group_by,
13864            group_by_all,
13865            having,
13866            unions: Vec::new(),
13867            order_by: Vec::new(),
13868            limit: None,
13869            offset: None,
13870            limit_with_ties: false,
13871            window_check_exprs,
13872        };
13873        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13874        // first set is the primary (already on stmt.group_by); each
13875        // further set becomes a UNION ALL peer with its dropped
13876        // keys (universe minus the set) replaced by NULL literals
13877        // in the peer's items and group_by. PG-legal: non-grouped
13878        // select items must be group keys or aggregates, so a
13879        // dropped key's occurrences in the projection are exactly
13880        // the ones to nullify.
13881        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13882        // over a plain GROUP BY (every argument must be a group key; the
13883        // mask is then 0) and rejects anything else with 42803. SPG's
13884        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13885        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13886        // function `grouping`".
13887        if grouping_sets.len() <= 1 {
13888            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13889            let mut calls: Vec<Expr> = Vec::new();
13890            for item in &stmt.items {
13891                if let SelectItem::Expr { expr, .. } = item {
13892                    Self::collect_grouping_calls(expr, &mut calls);
13893                }
13894            }
13895            if let Some(h) = &stmt.having {
13896                Self::collect_grouping_calls(h, &mut calls);
13897            }
13898            for call in &calls {
13899                let Expr::FunctionCall { args, .. } = call else {
13900                    continue;
13901                };
13902                for a in args {
13903                    if !keys.iter().any(|k| k == a) {
13904                        return Err(self.err(
13905                            "arguments to GROUPING must be grouping expressions of the associated query level"
13906                                .to_string(),
13907                        ));
13908                    }
13909                }
13910            }
13911            if !calls.is_empty() {
13912                for item in &mut stmt.items {
13913                    if let SelectItem::Expr { expr, .. } = item {
13914                        Self::substitute_grouping_calls(expr, &[]);
13915                    }
13916                }
13917                if let Some(h) = &mut stmt.having {
13918                    Self::substitute_grouping_calls(h, &[]);
13919                }
13920            }
13921        }
13922        if grouping_sets.len() > 1 {
13923            // The primary set's own dropped keys nullify in the
13924            // HEAD's projection too (GROUPING SETS's first set may
13925            // omit keys other sets use).
13926            let primary = grouping_sets[0].clone();
13927            let head_dropped: Vec<Expr> = grouping_universe
13928                .iter()
13929                .filter(|u| !primary.iter().any(|k| k == *u))
13930                .cloned()
13931                .collect();
13932            for set in grouping_sets.iter().skip(1) {
13933                let mut peer = stmt.clone();
13934                peer.unions = Vec::new();
13935                let dropped: Vec<&Expr> = grouping_universe
13936                    .iter()
13937                    .filter(|u| !set.iter().any(|k| k == *u))
13938                    .collect();
13939                // Empty set = grand-total group: `Some(vec![])` forces
13940                // the aggregate path (one collapsed row) instead of a
13941                // per-row passthrough. See the primary-set note above.
13942                peer.group_by = Some(set.clone());
13943                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13944                for item in &mut peer.items {
13945                    if let SelectItem::Expr { expr, alias } = item {
13946                        if dropped.iter().any(|d| *d == expr) {
13947                            // v7.39 — keep the dropped key's name on the
13948                            // NULL literal so the UNION output column
13949                            // (and any top-level ORDER BY on it) still
13950                            // resolves.
13951                            if alias.is_none()
13952                                && let Expr::Column(c) = &expr
13953                            {
13954                                *alias = Some(c.name.clone());
13955                            }
13956                            *expr = Expr::Literal(Literal::Null);
13957                        } else {
13958                            Self::substitute_grouping_calls(expr, &dropped_owned);
13959                        }
13960                    }
13961                }
13962                if let Some(h) = &mut peer.having {
13963                    Self::substitute_grouping_calls(h, &dropped_owned);
13964                }
13965                stmt.unions.push((UnionKind::All, peer));
13966            }
13967            for item in &mut stmt.items {
13968                if let SelectItem::Expr { expr, alias } = item {
13969                    if head_dropped.iter().any(|d| d == expr) {
13970                        if alias.is_none()
13971                            && let Expr::Column(c) = &expr
13972                        {
13973                            *alias = Some(c.name.clone());
13974                        }
13975                        *expr = Expr::Literal(Literal::Null);
13976                    } else {
13977                        Self::substitute_grouping_calls(expr, &head_dropped);
13978                    }
13979                }
13980            }
13981            if let Some(h) = &mut stmt.having {
13982                Self::substitute_grouping_calls(h, &head_dropped);
13983            }
13984            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13985            // (while `grouping_universe` / the per-branch sets are in scope). For
13986            // each grouping() call in it, inject a per-branch hidden column
13987            // `__grp_ord_K` carrying that branch's mask into the head + every
13988            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13989            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13990            // from the final output. A standalone grouping-set query has ORDER BY
13991            // (not an explicit set-op) next, so consuming it here is safe.
13992            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13993            // rollup carries the hierarchical order: sort by the grouping
13994            // keys with the rolled-up NULLs last, which is exactly the
13995            // interleaving both oracles emit. A client's own ORDER BY wins,
13996            // which is what MySQL does (MariaDB refuses to let one be
13997            // written at all).
13998            // The synthesised keys have to travel the SAME path a written
13999            // ORDER BY does: the block below is what turns a `grouping()`
14000            // call into the per-branch `__grp_ord_K` column the engine can
14001            // actually sort on. Bypassing it left a bare `grouping(text)`
14002            // for the evaluator to reject.
14003            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14004                self.parse_order_by_keys()?
14005            } else if mysql_rollup {
14006                Self::mysql_rollup_order(&grouping_universe)
14007            } else {
14008                Vec::new()
14009            };
14010            if !synthesised_or_parsed.is_empty() {
14011                let mut order_keys = synthesised_or_parsed;
14012                let mut grp_exprs: Vec<Expr> = Vec::new();
14013                for ob in &order_keys {
14014                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14015                }
14016                for (k, gexpr) in grp_exprs.iter().enumerate() {
14017                    let colname = alloc::format!("__grp_ord_{k}");
14018                    // Head branch (primary set) uses `head_dropped`.
14019                    let mut he = gexpr.clone();
14020                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14021                    stmt.items.push(SelectItem::Expr {
14022                        expr: he,
14023                        alias: Some(colname.clone()),
14024                    });
14025                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14026                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14027                        let set = &grouping_sets[i + 1];
14028                        let dropped: Vec<Expr> = grouping_universe
14029                            .iter()
14030                            .filter(|u| !set.iter().any(|k| k == *u))
14031                            .cloned()
14032                            .collect();
14033                        let mut pe = gexpr.clone();
14034                        Self::substitute_grouping_calls(&mut pe, &dropped);
14035                        peer.items.push(SelectItem::Expr {
14036                            expr: pe,
14037                            alias: Some(colname.clone()),
14038                        });
14039                    }
14040                }
14041                for ob in &mut order_keys {
14042                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14043                }
14044                stmt.order_by = order_keys;
14045            }
14046        }
14047        Ok(stmt)
14048    }
14049
14050    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14051    /// as ORDER BY keys.
14052    ///
14053    /// Per key: the rollup marker, then the key. Sorting on the key alone
14054    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14055    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14056    /// the ROLLUP-introduced NULL last, and both print as NULL.
14057    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14058    /// real group including the data-NULL one, 1 only for the row the
14059    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14060    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14061    ///
14062    /// `#[inline(never)]`: its locals must not join the statement parser's
14063    /// frame, which round 430 measured sitting against the nesting guard.
14064    #[inline(never)]
14065    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14066        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14067        for e in keys {
14068            out.push(OrderBy {
14069                expr: Expr::FunctionCall {
14070                    name: "grouping".into(),
14071                    args: alloc::vec![e.clone()],
14072                },
14073                desc: false,
14074                nulls_first: None,
14075                collation: None,
14076            });
14077            out.push(OrderBy {
14078                expr: e.clone(),
14079                desc: false,
14080                // MySQL orders NULL first on an ascending key.
14081                nulls_first: Some(true),
14082                collation: None,
14083            });
14084        }
14085        out
14086    }
14087
14088    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14089    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14090    #[inline(never)]
14091    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14092        use crate::ast::MaintainKind;
14093        self.skip_paren_option_list();
14094        let kind = match self.peek() {
14095            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14096            Token::Table | Token::Index => {
14097                self.advance();
14098                MaintainKind::ReindexRelation
14099            }
14100            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14101                "index" | "table" => {
14102                    self.advance();
14103                    MaintainKind::ReindexRelation
14104                }
14105                "schema" => {
14106                    self.advance();
14107                    MaintainKind::ReindexSchema
14108                }
14109                "system" | "database" => {
14110                    self.advance();
14111                    MaintainKind::Whole
14112                }
14113                // PG requires the object type; anything else is the
14114                // caller's problem, not something to swallow.
14115                _ => MaintainKind::ReindexRelation,
14116            },
14117            _ => MaintainKind::Whole,
14118        };
14119        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14120        // allows the plain form, so the modifier is recorded rather than
14121        // skipped. It still has no effect on how the reindex runs.
14122        let mut concurrently = false;
14123        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14124            self.advance();
14125            concurrently = true;
14126        }
14127        let target = self.take_optional_maintain_name();
14128        self.consume_until_statement_boundary();
14129        Ok(Statement::Maintain {
14130            kind,
14131            concurrently,
14132            target,
14133        })
14134    }
14135
14136    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14137    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14138    #[inline(never)]
14139    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14140        use crate::ast::MaintainKind;
14141        self.skip_paren_option_list();
14142        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14143            self.advance();
14144        }
14145        let target = self.take_optional_maintain_name();
14146        self.consume_until_statement_boundary();
14147        Ok(Statement::Maintain {
14148            kind: if target.is_some() {
14149                MaintainKind::ClusterRelation
14150            } else {
14151                MaintainKind::Whole
14152            },
14153            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14154            // transaction block quite happily (measured).
14155            concurrently: false,
14156            target,
14157        })
14158    }
14159
14160    /// The next token as a relation / schema name, when there is one.
14161    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14162        match self.peek() {
14163            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14164                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14165                _ => None,
14166            },
14167            _ => None,
14168        }
14169    }
14170
14171    /// A parenthesised option list, absorbed.
14172    fn skip_paren_option_list(&mut self) {
14173        if !matches!(self.peek(), Token::LParen) {
14174            return;
14175        }
14176        let mut depth = 0usize;
14177        loop {
14178            match self.advance() {
14179                Token::LParen => depth += 1,
14180                Token::RParen => {
14181                    depth -= 1;
14182                    if depth == 0 {
14183                        return;
14184                    }
14185                }
14186                Token::Eof => return,
14187                _ => {}
14188            }
14189        }
14190    }
14191
14192    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14193    /// column list.
14194    ///
14195    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14196    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14197    /// / ALL. The three that describe physical storage have no meaning
14198    /// here, so they parse and change nothing rather than making a
14199    /// dump that mentions them fail to load.
14200    ///
14201    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14202    /// parse chain the nesting sentinel is tuned against.
14203    #[inline(never)]
14204    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14205        self.advance(); // LIKE
14206        let source = self.expect_ident_like()?;
14207        let mut options = crate::ast::LikeOptions::default();
14208        loop {
14209            let including = match self.peek() {
14210                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14211                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14212                _ => break,
14213            };
14214            self.advance();
14215            // `ALL` lexes as its own keyword, not an identifier.
14216            let opt = if matches!(self.peek(), Token::All) {
14217                self.advance();
14218                alloc::string::String::from("all")
14219            } else {
14220                self.expect_ident_like()?
14221            };
14222            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14223                o.defaults = on;
14224                o.constraints = on;
14225                o.identity = on;
14226                o.generated = on;
14227                o.indexes = on;
14228                o.comments = on;
14229            };
14230            match opt.to_ascii_lowercase().as_str() {
14231                "all" => set(&mut options, including),
14232                "defaults" => options.defaults = including,
14233                "constraints" => options.constraints = including,
14234                "identity" => options.identity = including,
14235                "generated" => options.generated = including,
14236                "indexes" => options.indexes = including,
14237                "comments" => options.comments = including,
14238                // No storage model to copy into.
14239                "storage" | "statistics" | "compression" => {}
14240                other => {
14241                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14242                }
14243            }
14244        }
14245        Ok(crate::ast::LikeSpec {
14246            source,
14247            at,
14248            options,
14249        })
14250    }
14251
14252    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14253        // Caller already consumed CREATE; we're sitting on TABLE.
14254        debug_assert!(matches!(self.peek(), Token::Table));
14255        self.advance();
14256        let if_not_exists = self.consume_if_not_exists();
14257        let name = self.expect_ident_like()?;
14258        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14259        // child shape has no column list; the child inherits its
14260        // columns from the parent at engine-DDL time. Detect it
14261        // before the `(` requirement below.
14262        if matches!(self.peek(), Token::Partition)
14263            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14264        {
14265            self.advance(); // PARTITION
14266            self.advance(); // of
14267            let partition_of = self.parse_partition_of_tail()?;
14268            return Ok(Statement::CreateTable(CreateTableStatement {
14269                temporary: false,
14270                name,
14271                columns: Vec::new(),
14272                like_specs: Vec::new(),
14273                inherits: Vec::new(),
14274                if_not_exists,
14275                foreign_keys: Vec::new(),
14276                table_constraints: Vec::new(),
14277                partition_by: None,
14278                partition_of: Some(partition_of),
14279            }));
14280        }
14281        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14282        // the materialized-view materialisation path (run the SELECT, infer the
14283        // column types, create + populate the table) but marks the node so the
14284        // executor creates a plain table without a mat-view registry entry.
14285        if matches!(self.peek(), Token::As) {
14286            self.advance();
14287            let body_stmt = self.parse_select_stmt()?;
14288            let Statement::Select(body) = body_stmt else {
14289                return Err(self.err(format!(
14290                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14291                )));
14292            };
14293            let with_data = self.parse_optional_with_data(true)?;
14294            return Ok(Statement::CreateMaterializedView(
14295                crate::ast::CreateMaterializedViewStatement {
14296                    temporary: false,
14297                    name,
14298                    if_not_exists,
14299                    columns: Vec::new(),
14300                    body,
14301                    with_data,
14302                    as_plain_table: true,
14303                },
14304            ));
14305        }
14306        if !matches!(self.peek(), Token::LParen) {
14307            return Err(self.err(format!(
14308                "expected '(' after table name, got {:?}",
14309                self.peek()
14310            )));
14311        }
14312        self.advance();
14313        let mut columns = Vec::new();
14314        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14315        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14316        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14317        loop {
14318            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14319            // column list. It is how a child that adds nothing of its own is
14320            // written, and this loop demanded at least one entry: `syntax
14321            // error at or near ")"`. The child takes the parent's columns,
14322            // which the INHERITS clause already arranges.
14323            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14324                self.advance();
14325                break;
14326            }
14327            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14328            // clauses from column definitions. Constraints start
14329            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14330            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14331            // a column.
14332            if self.peek_table_level_pk_start() {
14333                table_constraints.push(self.parse_table_level_primary_key()?);
14334            } else if matches!(self.peek(), Token::Like) {
14335                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14336                // <opt> ]*`. The source table's shape lives in the catalog,
14337                // so this records the clause and the engine expands it.
14338                like_specs.push(self.parse_create_table_like(columns.len())?);
14339            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14340                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14341                table_constraints.push(self.parse_table_level_exclude()?);
14342            } else if self.peek_table_level_unique_start() {
14343                table_constraints.push(self.parse_table_level_unique()?);
14344            } else if self.peek_table_level_check_start() {
14345                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14346                table_constraints.push(self.parse_table_level_check()?);
14347            } else if self.peek_mysql_inline_key_start() {
14348                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14349                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14350                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14351                // inside the column list. Skip name + paren list;
14352                // for UNIQUE KEY, register as a UC.
14353                if let Some(uc) = self.parse_mysql_inline_key()? {
14354                    table_constraints.push(uc);
14355                }
14356            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14357                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14358                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14359                // CHECK is named, and the named-CONSTRAINT arm used
14360                // to accept FOREIGN KEY only. The name is accepted
14361                // and discarded — same handling as every other SPG
14362                // constraint name.
14363                self.advance(); // CONSTRAINT
14364                // v7.39 (read01 round 48) — the name is kept now: the schema
14365                // stores it, so DROP / RENAME CONSTRAINT can find it.
14366                let con_name = self.expect_ident_like()?;
14367                let mut tc = match kind {
14368                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14369                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14370                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14371                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14372                };
14373                match &mut tc {
14374                    crate::ast::TableConstraint::Check { name, .. }
14375                    | crate::ast::TableConstraint::Unique { name, .. }
14376                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14377                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14378                        *name = Some(con_name);
14379                    }
14380                    _ => {}
14381                }
14382                table_constraints.push(tc);
14383            } else if self.peek_constraint_or_fk_start() {
14384                foreign_keys.push(self.parse_table_level_fk()?);
14385            } else {
14386                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14387                // v7.13.0 — fold inline UNIQUE / CHECK column
14388                // constraints into table-level entries so the
14389                // engine path stays uniform.
14390                if col.is_unique {
14391                    table_constraints.push(crate::ast::TableConstraint::Unique {
14392                        name: None,
14393                        columns: alloc::vec![col.name.clone()],
14394                        nulls_not_distinct: col.unique_nulls_not_distinct,
14395                        deferrable: col.constraint_deferrable,
14396                        initially_deferred: col.constraint_initially_deferred,
14397                    });
14398                }
14399                if let Some(check_expr) = col.check.clone() {
14400                    table_constraints.push(crate::ast::TableConstraint::Check {
14401                        name: None,
14402                        expr: check_expr,
14403                        not_valid: false,
14404                    });
14405                }
14406                columns.push(col);
14407                if let Some(fk) = col_level_fk {
14408                    foreign_keys.push(fk);
14409                }
14410            }
14411            match self.peek() {
14412                Token::Comma => {
14413                    self.advance();
14414                }
14415                Token::RParen => {
14416                    self.advance();
14417                    break;
14418                }
14419                other => {
14420                    return Err(
14421                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14422                    );
14423                }
14424            }
14425        }
14426        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14427        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14428        // nothing is written between the parentheses.
14429        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14430        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14431        // empty parentheses were a parse error in their own right — quite apart
14432        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14433        // SPG does not have (filed separately).
14434        let _ = &like_specs;
14435        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14436        // It sits between the column list and the MySQL table options,
14437        // and it was a syntax error until this round.
14438        let mut inherits: Vec<String> = Vec::new();
14439        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14440            if k.eq_ignore_ascii_case("inherits"))
14441        {
14442            self.advance();
14443            if !matches!(self.peek(), Token::LParen) {
14444                return Err(self.err(alloc::format!(
14445                    "expected ( after INHERITS, got {:?}",
14446                    self.peek()
14447                )));
14448            }
14449            self.advance();
14450            loop {
14451                inherits.push(self.expect_ident_like()?);
14452                if matches!(self.peek(), Token::Comma) {
14453                    self.advance();
14454                    continue;
14455                }
14456                break;
14457            }
14458            if !matches!(self.peek(), Token::RParen) {
14459                return Err(self.err(alloc::format!(
14460                    "expected ) closing INHERITS, got {:?}",
14461                    self.peek()
14462                )));
14463            }
14464            self.advance();
14465        }
14466        // v7.14.0 — consume MySQL/MariaDB table options after the
14467        // closing `)`. mysqldump emits things like
14468        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14469        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14470        // SPG accepts all forms as no-ops (each option is
14471        // `<ident> [=] <ident-or-string>` separated by whitespace).
14472        self.consume_mysql_table_options();
14473        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14474        // SPG has no per-table reloptions, so accept and ignore them so a
14475        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14476        self.consume_with_reloptions();
14477        // v7.37.6-B — declarative-partition-parent suffix
14478        // (`PARTITION BY RANGE (key_col)`) sits after the column
14479        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14480        // and locks the key column at one ident; the engine then
14481        // verifies the column type is TIMESTAMPTZ.
14482        let partition_by = if matches!(self.peek(), Token::Partition) {
14483            self.advance(); // PARTITION
14484            if !self.peek_is_by() {
14485                return Err(self.err(format!(
14486                    "expected BY after PARTITION, got {:?}",
14487                    self.peek()
14488                )));
14489            }
14490            self.advance();
14491            Some(self.parse_partition_by_tail()?)
14492        } else {
14493            None
14494        };
14495        Ok(Statement::CreateTable(CreateTableStatement {
14496            temporary: false,
14497            name,
14498            columns,
14499            like_specs,
14500            inherits,
14501            if_not_exists,
14502            foreign_keys,
14503            table_constraints,
14504            partition_by,
14505            partition_of: None,
14506        }))
14507    }
14508
14509    /// v7.37.6-B — case-insensitive ident match helper for the
14510    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14511    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14512    /// didn't burn a global keyword slot for each (see the
14513    /// `Token::Partition` doc-comment in `lexer.rs`).
14514    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14515        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14516    }
14517
14518    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14519    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14520    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14521        use crate::ast::{PartitionBySpec, PartitionKindAst};
14522        let kind = match self.peek() {
14523            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14524                self.advance();
14525                PartitionKindAst::Range
14526            }
14527            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14528                self.advance();
14529                PartitionKindAst::List
14530            }
14531            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14532                self.advance();
14533                PartitionKindAst::Hash
14534            }
14535            other => {
14536                return Err(self.err(format!(
14537                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14538                )));
14539            }
14540        };
14541        if !matches!(self.peek(), Token::LParen) {
14542            return Err(self.err(format!(
14543                "expected '(' after PARTITION BY <strategy>, got {:?}",
14544                self.peek()
14545            )));
14546        }
14547        self.advance();
14548        let mut key_columns = Vec::new();
14549        loop {
14550            key_columns.push(self.expect_ident_like()?);
14551            match self.peek() {
14552                Token::Comma => {
14553                    self.advance();
14554                }
14555                Token::RParen => {
14556                    self.advance();
14557                    break;
14558                }
14559                other => {
14560                    return Err(self.err(format!(
14561                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14562                    )));
14563                }
14564            }
14565        }
14566        if key_columns.is_empty() {
14567            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14568        }
14569        Ok(PartitionBySpec { kind, key_columns })
14570    }
14571
14572    /// v7.37.6-B — after `PARTITION OF`, expect
14573    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14574    /// or
14575    ///   <parent> DEFAULT
14576    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14577        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14578        let parent_name = self.expect_ident_like()?;
14579        // v7.37.6-B rejects an explicit column list — the child
14580        // inherits from the parent. mailrs round-7 taught us that
14581        // CREATE TABLE-side schema reconciliation hides drift, so
14582        // we surface this as a parse error rather than silently
14583        // ignoring user columns.
14584        if matches!(self.peek(), Token::LParen) {
14585            return Err(self.err(
14586                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14587                 at v7.37.6-B; the child inherits its columns from the parent"
14588                    .to_string(),
14589            ));
14590        }
14591        let bounds = match self.peek() {
14592            Token::Default => {
14593                self.advance();
14594                PartitionOfBoundsAst::Default
14595            }
14596            Token::For => {
14597                self.advance();
14598                if !matches!(self.peek(), Token::Values) {
14599                    return Err(
14600                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14601                    );
14602                }
14603                self.advance();
14604                // WITH is not a reserved Token in the lexer — it lexes
14605                // as Token::Ident("with"). Disambiguate manually.
14606                let want_with = matches!(
14607                    self.peek(),
14608                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14609                );
14610                if want_with {
14611                    self.advance();
14612                    if !matches!(self.peek(), Token::LParen) {
14613                        return Err(self.err(format!(
14614                            "expected '(' after FOR VALUES WITH, got {:?}",
14615                            self.peek()
14616                        )));
14617                    }
14618                    self.advance();
14619                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14620                    loop {
14621                        let key = self.expect_ident_like()?;
14622                        let n = match self.peek().clone() {
14623                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14624                                self.advance();
14625                                v as u32
14626                            }
14627                            other => {
14628                                return Err(self.err(format!(
14629                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14630                                )));
14631                            }
14632                        };
14633                        match key.to_ascii_uppercase().as_str() {
14634                            "MODULUS" => modulus = Some(n),
14635                            "REMAINDER" => remainder = Some(n),
14636                            other => {
14637                                return Err(self.err(format!(
14638                                    "FOR VALUES WITH: unknown key {other:?}; \
14639                                     expected MODULUS or REMAINDER"
14640                                )));
14641                            }
14642                        }
14643                        match self.peek() {
14644                            Token::Comma => {
14645                                self.advance();
14646                            }
14647                            Token::RParen => {
14648                                self.advance();
14649                                break;
14650                            }
14651                            other => {
14652                                return Err(self.err(format!(
14653                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14654                                )));
14655                            }
14656                        }
14657                    }
14658                    let modulus = modulus
14659                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14660                    let remainder = remainder.ok_or_else(|| {
14661                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14662                    })?;
14663                    if modulus == 0 {
14664                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14665                    }
14666                    if remainder >= modulus {
14667                        return Err(self.err(format!(
14668                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14669                             must be < MODULUS ({modulus})"
14670                        )));
14671                    }
14672                    PartitionOfBoundsAst::Hash { modulus, remainder }
14673                } else {
14674                    match self.peek() {
14675                        Token::From => {
14676                            self.advance();
14677                            let lower = Box::new(self.parse_partition_bound_expr()?);
14678                            if !matches!(self.peek(), Token::To) {
14679                                return Err(self.err(format!(
14680                                    "expected TO after FROM (...), got {:?}",
14681                                    self.peek()
14682                                )));
14683                            }
14684                            self.advance();
14685                            let upper = Box::new(self.parse_partition_bound_expr()?);
14686                            PartitionOfBoundsAst::Range { lower, upper }
14687                        }
14688                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14689                        Token::In => {
14690                            self.advance();
14691                            if !matches!(self.peek(), Token::LParen) {
14692                                return Err(self.err(format!(
14693                                    "expected '(' after FOR VALUES IN, got {:?}",
14694                                    self.peek()
14695                                )));
14696                            }
14697                            self.advance();
14698                            let mut values = Vec::new();
14699                            loop {
14700                                values.push(self.parse_expr(0)?);
14701                                match self.peek() {
14702                                    Token::Comma => {
14703                                        self.advance();
14704                                    }
14705                                    Token::RParen => {
14706                                        self.advance();
14707                                        break;
14708                                    }
14709                                    other => {
14710                                        return Err(self.err(format!(
14711                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14712                                    )));
14713                                    }
14714                                }
14715                            }
14716                            if values.is_empty() {
14717                                return Err(self.err(
14718                                    "FOR VALUES IN requires at least one literal".to_string(),
14719                                ));
14720                            }
14721                            PartitionOfBoundsAst::List { values }
14722                        }
14723                        other => {
14724                            return Err(self.err(format!(
14725                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14726                            )));
14727                        }
14728                    }
14729                }
14730            }
14731            other => {
14732                return Err(self.err(format!(
14733                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14734                )));
14735            }
14736        };
14737        Ok(PartitionOfSpec {
14738            parent_name,
14739            bounds,
14740        })
14741    }
14742
14743    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14744    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14745    /// markers (no-arg builtins) so the engine resolves them
14746    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14747    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14748        if !matches!(self.peek(), Token::LParen) {
14749            return Err(self.err(format!(
14750                "expected '(' before partition bound, got {:?}",
14751                self.peek()
14752            )));
14753        }
14754        self.advance();
14755        let expr = match self.peek() {
14756            Token::Ident(s) | Token::QuotedIdent(s)
14757                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14758            {
14759                let name = s.to_ascii_uppercase();
14760                self.advance();
14761                crate::ast::Expr::FunctionCall {
14762                    name,
14763                    args: Vec::new(),
14764                }
14765            }
14766            _ => self.parse_expr(0)?,
14767        };
14768        if !matches!(self.peek(), Token::RParen) {
14769            return Err(self.err(format!(
14770                "expected ')' after partition bound, got {:?}",
14771                self.peek()
14772            )));
14773        }
14774        self.advance();
14775        Ok(expr)
14776    }
14777
14778    /// v7.14.0 — true when the next tokens look like an inline
14779    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14780    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14781    /// — each followed by an optional name + `(...)`. Critical:
14782    /// a column NAMED `key` / `index` (PG accepts as ident) must
14783    /// NOT be mistaken for the KEY constraint shape. We disambig
14784    /// by requiring the keyword to be followed by either `(` or
14785    /// `<ident> (`.
14786    fn peek_mysql_inline_key_start(&self) -> bool {
14787        let cur = self.peek();
14788        // Shapes:
14789        //   KEY (cols)
14790        //   KEY name (cols)
14791        //   INDEX (cols)
14792        //   INDEX name (cols)
14793        //   UNIQUE KEY [name] (cols)
14794        //   UNIQUE INDEX [name] (cols)
14795        //   FULLTEXT [KEY|INDEX] [name] (cols)
14796        //   SPATIAL [KEY|INDEX] [name] (cols)
14797        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14798            // tokens at skip = the position AFTER the index-form
14799            // keywords (KEY/INDEX) have been consumed.
14800            match self.tokens.get(skip) {
14801                Some(Token::LParen) => true,
14802                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14803                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14804                }
14805                _ => false,
14806            }
14807        };
14808        // `INDEX` lexes as Token::Index (reserved), not as
14809        // Token::Ident("index"). Both shapes count as a KEY/INDEX
14810        // start; the peek helper below handles either.
14811        let is_key_or_index_tok = |t: &Token| -> bool {
14812            matches!(t, Token::Index)
14813                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14814        };
14815        match cur {
14816            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14817            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14818                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14819            }
14820            Token::Ident(s)
14821                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14822            {
14823                let nxt = self.tokens.get(self.pos + 1);
14824                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14825                    self.pos + 2
14826                } else {
14827                    self.pos + 1
14828                };
14829                after_keyword_followed_by_paren_or_ident_paren(after_after)
14830            }
14831            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14832                let nxt = self.tokens.get(self.pos + 1);
14833                if !nxt.is_some_and(is_key_or_index_tok) {
14834                    return false;
14835                }
14836                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14837            }
14838            _ => false,
14839        }
14840    }
14841
14842    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14843    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14844    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14845    /// returns Some(TableConstraint::Index) so the engine builds
14846    /// a real BTree index on the leading column (mysqldump
14847    /// `KEY idx_posts_author (author_id)` shape).
14848    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14849    /// (the storage layer has no matching AM).
14850    fn parse_mysql_inline_key(
14851        &mut self,
14852    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14853        // Detect UNIQUE prefix.
14854        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14855        {
14856            self.advance();
14857            true
14858        } else {
14859            false
14860        };
14861        // Consume FULLTEXT / SPATIAL prefix and record which one
14862        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14863        // dedicated TableConstraint variant so the engine can
14864        // build a tsvector-GIN; SPATIAL still has no matching
14865        // AM, so it falls back to accept-as-no-op.
14866        let mut is_fulltext = false;
14867        let mut is_spatial = false;
14868        if let Token::Ident(s) = self.peek().clone() {
14869            if s.eq_ignore_ascii_case("fulltext") {
14870                self.advance();
14871                is_fulltext = true;
14872            } else if s.eq_ignore_ascii_case("spatial") {
14873                self.advance();
14874                is_spatial = true;
14875            }
14876        }
14877        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14878        // (reserved); accept either token shape.
14879        match self.peek() {
14880            Token::Index => {
14881                self.advance();
14882            }
14883            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14884                self.advance();
14885            }
14886            other => {
14887                return Err(self.err(alloc::format!(
14888                    "expected KEY/INDEX in inline index declaration, got {other:?}"
14889                )));
14890            }
14891        }
14892        // Optional index name (an ident before the `(`).
14893        // v7.15.0 — capture the name when present so the engine
14894        // builds the secondary index under the user's chosen
14895        // name (matches mysqldump's `KEY idx_x (col)` shape).
14896        let mut idx_name: Option<String> = None;
14897        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14898            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14899        {
14900            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14901                idx_name = Some(s);
14902            }
14903        }
14904        // Optional `USING BTREE` / `USING HASH` (MySQL).
14905        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14906            self.advance();
14907            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14908                self.advance();
14909            }
14910        }
14911        // Required column list `(col [, col]*)`.
14912        if !matches!(self.peek(), Token::LParen) {
14913            return Err(self.err(alloc::format!(
14914                "expected '(' in inline KEY/INDEX, got {:?}",
14915                self.peek()
14916            )));
14917        }
14918        self.advance();
14919        let mut cols: Vec<String> = Vec::new();
14920        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14921            self.advance();
14922            cols.push(s);
14923            // Skip optional `(length)` per-column prefix.
14924            if matches!(self.peek(), Token::LParen) {
14925                let mut depth = 1usize;
14926                self.advance();
14927                while depth > 0 {
14928                    match self.peek() {
14929                        Token::LParen => depth += 1,
14930                        Token::RParen => depth -= 1,
14931                        Token::Eof => break,
14932                        _ => {}
14933                    }
14934                    self.advance();
14935                }
14936            }
14937            // Skip optional ASC / DESC.
14938            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14939                || matches!(self.peek(), Token::Asc | Token::Desc)
14940            {
14941                self.advance();
14942            }
14943            if matches!(self.peek(), Token::Comma) {
14944                self.advance();
14945                continue;
14946            }
14947            break;
14948        }
14949        if matches!(self.peek(), Token::RParen) {
14950            self.advance();
14951        }
14952        // Trailing options on the inline index — comment / etc.
14953        // Skip until comma or `)`.
14954        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14955            self.advance();
14956        }
14957        if cols.is_empty() {
14958            return Ok(None);
14959        }
14960        if is_unique {
14961            // Carry the captured idx_name on UNIQUE too so future
14962            // engine work can name the underlying BTree
14963            // accordingly; today the unique-constraint installer
14964            // synthesises the name itself, but Display round-trip
14965            // benefits from preserving it.
14966            Ok(Some(crate::ast::TableConstraint::Unique {
14967                name: idx_name,
14968                columns: cols,
14969                nulls_not_distinct: false,
14970                // MySQL inline UNIQUE KEY has no deferral vocabulary.
14971                deferrable: false,
14972                initially_deferred: false,
14973            }))
14974        } else if is_fulltext {
14975            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14976            // routes through `TableConstraint::FulltextIndex`;
14977            // the engine builds a tsvector-GIN over each named
14978            // column so MATCH AGAINST gets a real inverted
14979            // index instead of a silently-dropped declaration.
14980            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14981                name: idx_name,
14982                columns: cols,
14983            }))
14984        } else if is_spatial {
14985            // SPG has no native SPATIAL AM. Accept-as-no-op
14986            // (declaration is parsed, but no index is built).
14987            Ok(None)
14988        } else {
14989            // v7.15.0 — plain KEY / INDEX builds a real BTree
14990            // secondary index.
14991            Ok(Some(crate::ast::TableConstraint::Index {
14992                name: idx_name,
14993                columns: cols,
14994            }))
14995        }
14996    }
14997
14998    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14999    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15000    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15001    /// (in any order, separated by whitespace).
15002    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15003    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15004    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15005    /// bare ident here, and only the parenthesised form is reloptions (so this
15006    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15007    fn consume_with_reloptions(&mut self) {
15008        let is_with = matches!(
15009            self.peek(),
15010            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15011        );
15012        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15013            return;
15014        }
15015        self.advance(); // WITH
15016        self.advance(); // (
15017        let mut depth = 1u32;
15018        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15019            match self.peek() {
15020                Token::LParen => depth += 1,
15021                Token::RParen => depth -= 1,
15022                _ => {}
15023            }
15024            self.advance();
15025        }
15026    }
15027
15028    fn consume_mysql_table_options(&mut self) {
15029        loop {
15030            // Heuristic: a table option is an ident (or `DEFAULT`
15031            // reserved keyword) followed by `=` and an
15032            // ident / string / integer.
15033            let name_lc = match self.peek().clone() {
15034                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15035                Token::Default => alloc::string::String::from("default"),
15036                _ => break,
15037            };
15038            let known = matches!(
15039                name_lc.as_str(),
15040                "engine"
15041                    | "default"
15042                    | "charset"
15043                    | "collate"
15044                    | "auto_increment"
15045                    | "row_format"
15046                    | "comment"
15047                    | "pack_keys"
15048                    | "stats_persistent"
15049                    | "stats_auto_recalc"
15050                    | "stats_sample_pages"
15051                    | "key_block_size"
15052                    | "tablespace"
15053                    | "min_rows"
15054                    | "max_rows"
15055                    | "checksum"
15056                    | "delay_key_write"
15057                    | "insert_method"
15058                    | "data"
15059                    | "index"
15060                    | "encryption"
15061                    | "compression"
15062            );
15063            if !known {
15064                break;
15065            }
15066            self.advance(); // option name
15067            // `DEFAULT` optional prefix is followed by `CHARSET` /
15068            // `COLLATE`; consume the next ident too.
15069            if name_lc == "default" {
15070                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15071                    self.advance();
15072                }
15073            }
15074            if matches!(self.peek(), Token::Eq) {
15075                self.advance();
15076            }
15077            match self.peek() {
15078                Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
15079                    self.advance();
15080                }
15081                _ => {}
15082            }
15083        }
15084    }
15085
15086    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15087    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15088    /// sure (otherwise a column literally named `primary` would
15089    /// be mistaken).
15090    fn peek_table_level_pk_start(&self) -> bool {
15091        let cur = self.peek();
15092        let nxt = self.tokens.get(self.pos + 1);
15093        let nxt2 = self.tokens.get(self.pos + 2);
15094        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15095        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15096        let is_lparen = matches!(nxt2, Some(Token::LParen));
15097        is_primary && is_key && is_lparen
15098    }
15099
15100    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15101    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15102    /// (mailrs round-5 G10).
15103    fn peek_table_level_unique_start(&self) -> bool {
15104        let cur = self.peek();
15105        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15106        if !is_unique {
15107            return false;
15108        }
15109        let n1 = self.tokens.get(self.pos + 1);
15110        // Plain `UNIQUE (…)`.
15111        if matches!(n1, Some(Token::LParen)) {
15112            return true;
15113        }
15114        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15115        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15116        if !is_nulls {
15117            return false;
15118        }
15119        let n2 = self.tokens.get(self.pos + 2);
15120        let n3 = self.tokens.get(self.pos + 3);
15121        let n4 = self.tokens.get(self.pos + 4);
15122        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15123        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15124            return true;
15125        }
15126        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15127        if matches!(n2, Some(Token::Not))
15128            && matches!(n3, Some(Token::Distinct))
15129            && matches!(n4, Some(Token::LParen))
15130        {
15131            return true;
15132        }
15133        false
15134    }
15135
15136    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15137        self.advance(); // PRIMARY
15138        self.advance(); // KEY
15139        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15140        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15141        // 621 consumed and dropped them (the storing half of F08).
15142        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15143        Ok(crate::ast::TableConstraint::PrimaryKey {
15144            name: None,
15145            columns,
15146            deferrable,
15147            initially_deferred,
15148        })
15149    }
15150
15151    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15152        self.advance(); // UNIQUE
15153        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15154        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15155        // is `NULLS DISTINCT` per the SQL standard.
15156        let mut nulls_not_distinct = false;
15157        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15158            let n1 = self.tokens.get(self.pos + 1);
15159            let n2 = self.tokens.get(self.pos + 2);
15160            let is_not = matches!(n1, Some(Token::Not));
15161            let is_distinct = matches!(n2, Some(Token::Distinct));
15162            if is_not && is_distinct {
15163                self.advance(); // NULLS
15164                self.advance(); // NOT
15165                self.advance(); // DISTINCT
15166                nulls_not_distinct = true;
15167            } else if matches!(n1, Some(Token::Distinct)) {
15168                self.advance(); // NULLS
15169                self.advance(); // DISTINCT
15170            }
15171        }
15172        let columns = self.parse_paren_ident_list("UNIQUE")?;
15173        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15174        Ok(crate::ast::TableConstraint::Unique {
15175            name: None,
15176            columns,
15177            nulls_not_distinct,
15178            deferrable,
15179            initially_deferred,
15180        })
15181    }
15182
15183    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15184    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15185    /// expression.
15186    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15187    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15188    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15189    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15190    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15191    /// commit: `NOT` starts no other suffix here, but reading both
15192    /// tokens before advancing keeps the caller's error message intact
15193    /// if someone writes `NOT NULL` by mistake.
15194    fn parse_not_valid_suffix(&mut self) -> bool {
15195        if !matches!(self.peek(), Token::Not) {
15196            return false;
15197        }
15198        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15199        {
15200            return false;
15201        }
15202        self.advance();
15203        self.advance();
15204        true
15205    }
15206
15207    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15208        self.advance(); // EXCLUDE
15209        // Optional `USING <method>`.
15210        let mut method = None;
15211        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15212            self.advance();
15213            method = Some(match self.advance() {
15214                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15215                other => {
15216                    return Err(self.err(alloc::format!(
15217                        "expected index method after USING, got {other:?}"
15218                    )));
15219                }
15220            });
15221        }
15222        if !matches!(self.peek(), Token::LParen) {
15223            return Err(self.err(alloc::format!(
15224                "expected '(' after EXCLUDE, got {:?}",
15225                self.peek()
15226            )));
15227        }
15228        self.advance();
15229        let mut elements: Vec<(String, String)> = Vec::new();
15230        loop {
15231            let col = match self.advance() {
15232                Token::Ident(s) | Token::QuotedIdent(s) => s,
15233                other => {
15234                    return Err(self.err(alloc::format!(
15235                        "expected column name in EXCLUDE, got {other:?}"
15236                    )));
15237                }
15238            };
15239            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15240                return Err(self.err(alloc::format!(
15241                    "expected WITH after EXCLUDE column, got {:?}",
15242                    self.peek()
15243                )));
15244            }
15245            self.advance();
15246            let op = match self.advance() {
15247                Token::InetOverlap => String::from("&&"),
15248                Token::Intersects => String::from("?#"),
15249                Token::IsBelow => String::from("<^"),
15250                Token::IsAbove => String::from(">^"),
15251                Token::PatternLt => String::from("~<~"),
15252                Token::PatternLtEq => String::from("~<=~"),
15253                Token::PatternGt => String::from("~>~"),
15254                Token::PatternGtEq => String::from("~>=~"),
15255                Token::TsMatchOld => String::from("@@@"),
15256                Token::Eq => String::from("="),
15257                Token::JsonContains => String::from("@>"),
15258                Token::JsonContainedBy => String::from("<@"),
15259                Token::OverLeft => String::from("&<"),
15260                Token::OverRight => String::from("&>"),
15261                other => {
15262                    return Err(self.err(alloc::format!(
15263                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15264                    )));
15265                }
15266            };
15267            elements.push((col, op));
15268            if matches!(self.peek(), Token::Comma) {
15269                self.advance();
15270                continue;
15271            }
15272            break;
15273        }
15274        if !matches!(self.peek(), Token::RParen) {
15275            return Err(self.err(alloc::format!(
15276                "expected ')' to close EXCLUDE, got {:?}",
15277                self.peek()
15278            )));
15279        }
15280        self.advance();
15281        Ok(crate::ast::TableConstraint::Exclude {
15282            name: None,
15283            method,
15284            elements,
15285        })
15286    }
15287
15288    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15289        self.advance(); // CHECK
15290        if !matches!(self.peek(), Token::LParen) {
15291            return Err(self.err(alloc::format!(
15292                "expected '(' after CHECK, got {:?}",
15293                self.peek()
15294            )));
15295        }
15296        self.advance();
15297        let expr = self.parse_expr(0)?;
15298        if !matches!(self.peek(), Token::RParen) {
15299            return Err(self.err(alloc::format!(
15300                "expected ')' to close CHECK predicate, got {:?}",
15301                self.peek()
15302            )));
15303        }
15304        self.advance();
15305        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15306        // are no existing rows for PG to skip, so it rejects the suffix.
15307        Ok(crate::ast::TableConstraint::Check {
15308            name: None,
15309            expr,
15310            not_valid: false,
15311        })
15312    }
15313
15314    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15315    fn peek_table_level_check_start(&self) -> bool {
15316        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15317    }
15318
15319    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15320    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15321    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15322    /// own CONSTRAINT prefix).
15323    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15324        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15325            return None;
15326        }
15327        // tokens[pos+1] is the constraint name (any ident-like);
15328        // tokens[pos+2] is the kind keyword.
15329        match self.tokens.get(self.pos + 2) {
15330            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15331                Some(NamedTableConstraintKind::Check)
15332            }
15333            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15334                Some(NamedTableConstraintKind::Unique)
15335            }
15336            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15337                Some(NamedTableConstraintKind::PrimaryKey)
15338            }
15339            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15340                Some(NamedTableConstraintKind::Exclude)
15341            }
15342            _ => None,
15343        }
15344    }
15345
15346    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15347        if !matches!(self.peek(), Token::LParen) {
15348            return Err(self.err(alloc::format!(
15349                "expected '(' after {ctx}, got {:?}",
15350                self.peek()
15351            )));
15352        }
15353        self.advance();
15354        let mut out = Vec::new();
15355        loop {
15356            out.push(self.expect_ident_like()?);
15357            match self.peek() {
15358                Token::Comma => {
15359                    self.advance();
15360                }
15361                Token::RParen => {
15362                    self.advance();
15363                    break;
15364                }
15365                other => {
15366                    return Err(self.err(alloc::format!(
15367                        "expected ',' or ')' in {ctx} list, got {other:?}"
15368                    )));
15369                }
15370            }
15371        }
15372        if out.is_empty() {
15373            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15374        }
15375        Ok(out)
15376    }
15377
15378    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15379    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15380    /// table-level FK; a column def never starts with either keyword
15381    /// (column names are not in this reserved set).
15382    fn peek_constraint_or_fk_start(&self) -> bool {
15383        let is_constraint_kw = matches!(
15384            self.peek(),
15385            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15386        );
15387        let is_foreign_kw = matches!(
15388            self.peek(),
15389            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15390        );
15391        is_constraint_kw || is_foreign_kw
15392    }
15393
15394    /// v7.6.0 — parse a table-level FK clause:
15395    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15396    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15397    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15398        let mut name: Option<String> = None;
15399        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15400            self.advance();
15401            name = Some(self.expect_ident_like()?);
15402        }
15403        // `FOREIGN`
15404        match self.advance() {
15405            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15406            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15407        }
15408        // `KEY`
15409        match self.advance() {
15410            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15411            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15412        }
15413        // `(col, col, ...)`
15414        if !matches!(self.peek(), Token::LParen) {
15415            return Err(self.err(format!(
15416                "expected '(' after FOREIGN KEY, got {:?}",
15417                self.peek()
15418            )));
15419        }
15420        self.advance();
15421        let mut columns = Vec::new();
15422        loop {
15423            columns.push(self.expect_ident_like()?);
15424            match self.peek() {
15425                Token::Comma => {
15426                    self.advance();
15427                }
15428                Token::RParen => {
15429                    self.advance();
15430                    break;
15431                }
15432                other => {
15433                    return Err(self.err(format!(
15434                        "expected ',' or ')' in FK column list, got {other:?}"
15435                    )));
15436                }
15437            }
15438        }
15439        if columns.is_empty() {
15440            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15441        }
15442        let (
15443            parent_table,
15444            parent_columns,
15445            on_delete,
15446            on_update,
15447            match_type,
15448            deferrable,
15449            initially_deferred,
15450        ) = self.parse_references_tail(columns.len())?;
15451        Ok(ForeignKeyConstraint {
15452            name,
15453            columns,
15454            parent_table,
15455            parent_columns,
15456            on_delete,
15457            on_update,
15458            match_type,
15459            deferrable,
15460            initially_deferred,
15461        })
15462    }
15463
15464    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15465    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15466    /// the local column count, used to default the parent column
15467    /// list when omitted (SQL spec: parent's PK is implied).
15468    fn parse_references_tail(
15469        &mut self,
15470        expected_arity: usize,
15471    ) -> Result<
15472        (
15473            String,
15474            Vec<String>,
15475            FkAction,
15476            FkAction,
15477            crate::ast::MatchType,
15478            // v7.39 (round 288) — deferrable, initially_deferred.
15479            bool,
15480            bool,
15481        ),
15482        ParseError,
15483    > {
15484        match self.advance() {
15485            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15486            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15487        }
15488        let parent_table = self.expect_ident_like()?;
15489        let mut parent_columns: Vec<String> = Vec::new();
15490        if matches!(self.peek(), Token::LParen) {
15491            self.advance();
15492            loop {
15493                parent_columns.push(self.expect_ident_like()?);
15494                match self.peek() {
15495                    Token::Comma => {
15496                        self.advance();
15497                    }
15498                    Token::RParen => {
15499                        self.advance();
15500                        break;
15501                    }
15502                    other => {
15503                        return Err(self.err(format!(
15504                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15505                        )));
15506                    }
15507                }
15508            }
15509        }
15510        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15511            return Err(self.err(format!(
15512                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15513                expected_arity,
15514                parent_columns.len()
15515            )));
15516        }
15517        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15518        // it between the referenced column list and the ON / DEFERRABLE
15519        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15520        // is skipped when any referencing column is NULL), so SIMPLE —
15521        // the default, and the only spelling pg_dump emits — is accepted
15522        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15523        // mixed-NULL rule, which is not wired yet; reject them honestly
15524        // rather than silently applying SIMPLE (PG itself errors on
15525        // MATCH PARTIAL as "not yet implemented").
15526        let mut match_type = crate::ast::MatchType::Simple;
15527        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15528            self.advance();
15529            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15530            // SIMPLE / PARTIAL arrive as bare identifiers.
15531            let kind = match self.advance() {
15532                Token::Full => "FULL".to_string(),
15533                Token::Ident(s) => s.to_uppercase(),
15534                other => {
15535                    return Err(self.err(format!(
15536                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15537                    )));
15538                }
15539            };
15540            match kind.as_str() {
15541                "SIMPLE" => {} // Default — match_type stays Simple.
15542                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15543                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15544                "FULL" => match_type = crate::ast::MatchType::Full,
15545                "PARTIAL" => {
15546                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15547                }
15548                _ => {
15549                    return Err(self.err(format!(
15550                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15551                    )));
15552                }
15553            }
15554        }
15555        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15556        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15557        // <action>` / `ON UPDATE <action>` in either order. PG /
15558        // pg_dump emits the timing clause AFTER the ON clauses
15559        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15560        // but the SQL spec allows either order. We loop over
15561        // every possible trailer and dispatch on the next token,
15562        // stopping when nothing matches. Phase 3.1 changes the
15563        // bare DEFERRABLE form from hard-error to accept-as-
15564        // immediate; SPG is single-writer with no deferred-
15565        // constraint window so the runtime semantics are always
15566        // immediate even when INITIALLY DEFERRED is requested.
15567        // PG's default referential action (no ON DELETE / ON UPDATE
15568        // clause) is NO ACTION, not RESTRICT — the two enforce
15569        // identically in SPG (single-writer, no deferred window; see the
15570        // shared match arm in constraints.rs) but information_schema.
15571        // referential_constraints must report NO ACTION to match PG.
15572        let mut on_delete = FkAction::NoAction;
15573        let mut on_update = FkAction::NoAction;
15574        let mut seen_on_delete = false;
15575        let mut seen_on_update = false;
15576        let mut deferrable = false;
15577        let mut initially_deferred = false;
15578        loop {
15579            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15580            let before = self.pos;
15581            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15582            if self.pos != before {
15583                deferrable = d;
15584                initially_deferred = idef;
15585                continue;
15586            }
15587            // ON DELETE / ON UPDATE.
15588            if !matches!(self.peek(), Token::On) {
15589                break;
15590            }
15591            self.advance();
15592            let which = self.advance();
15593            let action = self.parse_fk_action()?;
15594            match which {
15595                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15596                    if seen_on_delete {
15597                        return Err(self.err("ON DELETE specified twice".into()));
15598                    }
15599                    seen_on_delete = true;
15600                    on_delete = action;
15601                }
15602                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15603                    if seen_on_update {
15604                        return Err(self.err("ON UPDATE specified twice".into()));
15605                    }
15606                    seen_on_update = true;
15607                    on_update = action;
15608                }
15609                other => {
15610                    return Err(
15611                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15612                    );
15613                }
15614            }
15615        }
15616        Ok((
15617            parent_table,
15618            parent_columns,
15619            on_delete,
15620            on_update,
15621            match_type,
15622            deferrable,
15623            initially_deferred,
15624        ))
15625    }
15626
15627    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15628    /// NO ACTION`.
15629    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15630        match self.advance() {
15631            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15632            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15633            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15634                Token::Null => Ok(FkAction::SetNull),
15635                Token::Default => Ok(FkAction::SetDefault),
15636                other => Err(self.err(format!(
15637                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15638                ))),
15639            },
15640            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15641                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15642                other => Err(self.err(format!(
15643                    "expected ACTION after NO in FK action, got {other:?}"
15644                ))),
15645            },
15646            other => Err(self.err(format!(
15647                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15648            ))),
15649        }
15650    }
15651
15652    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15653    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15654    fn consume_if_not_exists(&mut self) -> bool {
15655        // `IF` arrives as a bare Ident (we don't reserve it because it
15656        // also appears mid-expression in PG, though we don't support
15657        // those forms yet).
15658        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15659        if !looks_like_if {
15660            return false;
15661        }
15662        // Peek one ahead before committing: only consume IF when it's
15663        // actually `IF NOT EXISTS`.
15664        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15665            return false;
15666        }
15667        if !matches!(
15668            self.tokens.get(self.pos + 2),
15669            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15670        ) {
15671            return false;
15672        }
15673        self.advance(); // IF
15674        self.advance(); // NOT
15675        self.advance(); // EXISTS
15676        true
15677    }
15678
15679    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15680    /// Consumes IF EXISTS as a pair; returns false otherwise
15681    /// without consuming any tokens.
15682    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15683    /// ENABLE/DISABLE/FORCE/NO FORCE.
15684    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15685        for kw in ["row", "level", "security"] {
15686            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15687            {
15688                return Err(self.err(alloc::format!(
15689                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15690                    kw.to_ascii_uppercase(),
15691                    self.peek()
15692                )));
15693            }
15694            self.advance();
15695        }
15696        Ok(())
15697    }
15698
15699    fn consume_if_exists(&mut self) -> bool {
15700        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15701        if !looks_like_if {
15702            return false;
15703        }
15704        if !matches!(
15705            self.tokens.get(self.pos + 1),
15706            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15707        ) {
15708            return false;
15709        }
15710        self.advance(); // IF
15711        self.advance(); // EXISTS
15712        true
15713    }
15714
15715    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15716    /// qualifiers after an index column ref. ASC / DESC are
15717    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15718    /// We accept and discard them since single-column BTree
15719    /// stores rows in natural key order today.
15720    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15721    /// ORDER BY key. Returns None when absent.
15722    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15723        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15724            return Ok(None);
15725        }
15726        self.advance();
15727        match self.advance() {
15728            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15729            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15730            other => Err(self.err(alloc::format!(
15731                "expected FIRST or LAST after NULLS, got {other:?}"
15732            ))),
15733        }
15734    }
15735
15736    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15737    /// rather than discarded.
15738    ///
15739    /// SPG's index does not scan in a direction — column ordering is
15740    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15741    /// reproduction of the DDL, and dropping the clause meant
15742    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15743    /// dump lost it, and a schema diff saw drift on every run.
15744    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15745        let mut order = crate::ast::IndexColumnOrder::default();
15746        loop {
15747            match self.peek() {
15748                Token::Asc => {
15749                    self.advance();
15750                }
15751                Token::Desc => {
15752                    order.descending = true;
15753                    self.advance();
15754                }
15755                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15756                    let look = self.tokens.get(self.pos + 1);
15757                    if matches!(
15758                        look,
15759                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15760                            || k.eq_ignore_ascii_case("last")
15761                    ) {
15762                        self.advance();
15763                        order.nulls_first = Some(matches!(
15764                            self.advance(),
15765                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
15766                        ));
15767                    } else {
15768                        break;
15769                    }
15770                }
15771                _ => break,
15772            }
15773        }
15774        order
15775    }
15776
15777    fn parse_create_index_stmt_after_create(
15778        &mut self,
15779        is_unique: bool,
15780    ) -> Result<Statement, ParseError> {
15781        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15782        debug_assert!(matches!(self.peek(), Token::Index));
15783        self.advance();
15784        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15785        // SPG's CREATE INDEX is synchronous end-to-end today (real
15786        // CONCURRENTLY variant with restartable scans queues with
15787        // v7.39 indexes epic), so the modifier has no runtime effect
15788        // — same accept-and-no-op shape as v7.37.16.5 DETACH
15789        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15790        // VIEW CONCURRENTLY.
15791        let mut concurrently = false;
15792        if matches!(
15793            self.peek(),
15794            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15795        ) {
15796            self.advance();
15797            concurrently = true;
15798        }
15799        let if_not_exists = self.consume_if_not_exists();
15800        // v7.39 (read01 round 93) — the index name is optional (PG since
15801        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15802        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15803        // was given; leave it empty and the engine derives a PG-style
15804        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15805        let name = if matches!(self.peek(), Token::On) {
15806            String::new()
15807        } else {
15808            self.expect_ident_like()?
15809        };
15810        if !matches!(self.peek(), Token::On) {
15811            return Err(self.err(format!(
15812                "expected ON after CREATE INDEX <name>, got {:?}",
15813                self.peek()
15814            )));
15815        }
15816        self.advance();
15817        let table = self.expect_ident_like()?;
15818        // Optional `USING <method>` — only recognised method in v2.0 is
15819        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15820        // ident `using` (we don't promote it to a reserved keyword
15821        // because it isn't reserved anywhere else in our SQL surface).
15822        let mut method_name: Option<String> = None;
15823        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15824            self.advance();
15825            let m = self.expect_ident_like()?;
15826            method_name = Some(m.to_ascii_lowercase());
15827            match m.to_ascii_lowercase().as_str() {
15828                "hnsw" => IndexMethod::Hnsw,
15829                "btree" => IndexMethod::BTree,
15830                "brin" => IndexMethod::Brin,
15831                // v7.12.3 — real GIN inverted index over `tsvector`.
15832                // v7.9.26b's `USING gin` → BTree silent fallback is
15833                // gone; the engine validates that the indexed column
15834                // is `tsvector` at CREATE INDEX time.
15835                "gin" => IndexMethod::Gin,
15836                // v7.9.26b — PG `pg_dump` emits `USING gist` /
15837                // `USING spgist` / `USING hash` for their built-in
15838                // AMs that SPG doesn't have a matching
15839                // implementation for; degrade to BTree on the
15840                // leading column so the schema loads + the index
15841                // catalogue stays consistent. Operator pays the
15842                // planner cost only for the queries that would have
15843                // used the specialised AM.
15844                "gist" | "spgist" | "hash" => IndexMethod::BTree,
15845                // v7.11.3 — pgvector ships both `ivfflat` and
15846                // `hnsw`. Customers shouldn't have to choose
15847                // their on-disk index method based on what SPG
15848                // implements; accept `ivfflat` as a synonym for
15849                // `hnsw` so PG schemas using either method drop
15850                // in. The vector distance op (`<->` / `<#>` /
15851                // `<=>`) at query time still picks the metric.
15852                "ivfflat" => IndexMethod::Hnsw,
15853                other => {
15854                    return Err(self.err(alloc::format!(
15855                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15856                    )));
15857                }
15858            }
15859        } else {
15860            IndexMethod::BTree
15861        };
15862        if !matches!(self.peek(), Token::LParen) {
15863            return Err(self.err(format!(
15864                "expected '(' before indexed column, got {:?}",
15865                self.peek()
15866            )));
15867        }
15868        self.advance();
15869        // v6.8.2 — accept either a bare column ident (legacy) or
15870        // an expression `fn(col, …)` for expression indexes.
15871        // Distinguish by peeking the token *after* the current
15872        // ident: `ident )` is the legacy column-only path;
15873        // anything else triggers the Pratt expression parser.
15874        // (`advance()` uses `mem::replace` to nil out the current
15875        // slot, so we can't save+rewind cleanly — peek-ahead via
15876        // direct index avoids the mutation.)
15877        let mut opclass: Option<String> = None;
15878        let mut key_collation: Option<String> = None;
15879        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15880            // Single column with `)` immediately after — fast path.
15881            // v7.9.29 — also: bare column followed by `,` (the
15882            // multi-column form `(a, b, c)`). Without this branch
15883            // the leading ident gets pulled into `parse_expr`
15884            // which then sets `expression = Some(Column(a))` and
15885            // breaks Display round-trip on the multi-column shape.
15886            Token::Ident(s) | Token::QuotedIdent(s)
15887                if matches!(
15888                    self.tokens.get(self.pos + 1),
15889                    Some(Token::RParen | Token::Comma)
15890                ) =>
15891            {
15892                self.advance();
15893                (s, None)
15894            }
15895            // v7.9.22 — single column followed by a pgvector
15896            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15897            // v7.15.0 — capture the opclass instead of discarding
15898            // it so the engine can dispatch (e.g. `gin_trgm_ops`
15899            // → real trigram-shingle GIN over a TEXT column).
15900            // Vector/HNSW opclasses still take their distance
15901            // metric from the query operator (`<->` / `<#>` /
15902            // `<=>`), so for those callers the opclass stays
15903            // informational.
15904            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15905            // opclass: `(embedding public.vector_cosine_ops)`. Strip
15906            // the schema and dispatch on the bare opclass, the same
15907            // treatment table/type names get.
15908            Token::Ident(s) | Token::QuotedIdent(s)
15909                if matches!(
15910                    self.tokens.get(self.pos + 1),
15911                    Some(Token::Ident(_) | Token::QuotedIdent(_))
15912                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15913                    && matches!(
15914                        self.tokens.get(self.pos + 3),
15915                        Some(Token::Ident(op) | Token::QuotedIdent(op))
15916                            if is_vector_opclass_name(op)
15917                    ) =>
15918            {
15919                self.advance(); // column name
15920                self.advance(); // schema qualifier
15921                self.advance(); // dot
15922                let op_tok = self.advance();
15923                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15924                    opclass = Some(op.to_ascii_lowercase());
15925                }
15926                (s, None)
15927            }
15928            // r1038 — an operator class is recognised by its POSITION, not
15929            // by a list of names. It used to be `is_vector_opclass_name`,
15930            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15931            // sentori's migration wrote — was a syntax error while
15932            // `USING gin (doc)` parsed. Anything sitting between a column
15933            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15934            // two bare identifiers in a row are not valid there otherwise.
15935            Token::Ident(s) | Token::QuotedIdent(s)
15936                if matches!(
15937                    self.tokens.get(self.pos + 1),
15938                    Some(Token::Ident(op) | Token::QuotedIdent(op))
15939                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
15940                            self.tokens.get(self.pos + 2)
15941                        )
15942                ) =>
15943            {
15944                self.advance(); // column name
15945                // Capture the opclass token, lower-cased for
15946                // case-insensitive engine dispatch.
15947                let op_tok = self.advance();
15948                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15949                    opclass = Some(op.to_ascii_lowercase());
15950                }
15951                (s, None)
15952            }
15953            Token::Ident(_) | Token::QuotedIdent(_) => {
15954                // v7.39 (round 538) — an explicit COLLATE on the key,
15955                // read by LOOKAHEAD because `parse_expr` absorbs the
15956                // clause as a no-op (SPG orders text by bytes, which is
15957                // the C collation, so it changes nothing to honour). PG
15958                // still PRINTS it: an explicitly written `"C"` and the
15959                // collation a column inherits are different collation
15960                // OBJECTS even where they sort identically, which is why
15961                // `(a COLLATE "C")` shows on a C-collation database too.
15962                if matches!(
15963                    self.tokens.get(self.pos + 1),
15964                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15965                ) {
15966                    key_collation = match self.tokens.get(self.pos + 2) {
15967                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15968                            Some(n.clone())
15969                        }
15970                        _ => None,
15971                    };
15972                }
15973                let key_expr = self.parse_expr(0)?;
15974                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15975                    self.err("expression index key must reference at least one column".into())
15976                })?;
15977                (primary, Some(key_expr))
15978            }
15979            // v7.37.43-T4 — parenthesised expression index key
15980            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15981            // PG's CREATE INDEX requires the expression to be in
15982            // its own parens to disambiguate function calls from
15983            // column lists, so this `LParen` is the inner open-paren
15984            // of an expression key. parse_expr handles the recursive
15985            // descent and consumes the matching `RParen`.
15986            Token::LParen => {
15987                let key_expr = self.parse_expr(0)?;
15988                let primary = extract_first_column(&key_expr).ok_or_else(|| {
15989                    self.err("expression index key must reference at least one column".into())
15990                })?;
15991                (primary, Some(key_expr))
15992            }
15993            other => {
15994                return Err(self.err(format!(
15995                    "expected column ident or expression, got {other:?}"
15996                )));
15997            }
15998        };
15999        // v7.9.14 — accept extra comma-separated columns inside
16000        // the index key parens (`CREATE INDEX … (a, b, c)`).
16001        // mailrs F2. Each extra column may carry an optional
16002        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16003        // — parsed and discarded; SPG doesn't honour direction
16004        // on a BTree index today (column ordering is intrinsic
16005        // to the storage). v7.10 will widen to genuine composite
16006        // index keys.
16007        let mut extra_columns: Vec<String> = Vec::new();
16008        // The leading column may also have ASC/DESC after it — and that
16009        // one is the column SPG indexes, so its clause is kept.
16010        let key_order = self.consume_optional_index_column_qualifiers();
16011        while matches!(self.peek(), Token::Comma) {
16012            self.advance();
16013            let extra = self.expect_ident_like()?;
16014            let _ = self.consume_optional_index_column_qualifiers();
16015            extra_columns.push(extra);
16016        }
16017        if !matches!(self.peek(), Token::RParen) {
16018            return Err(self.err(format!(
16019                "expected ')' after indexed column / expression, got {:?}",
16020                self.peek()
16021            )));
16022        }
16023        self.advance();
16024        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16025        // index-only-scan annotation. Bare ident (not a reserved
16026        // keyword) so we test by case-insensitive string match.
16027        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16028        {
16029            self.advance();
16030            if !matches!(self.peek(), Token::LParen) {
16031                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16032            }
16033            self.advance();
16034            let mut cols = Vec::new();
16035            loop {
16036                cols.push(self.expect_ident_like()?);
16037                match self.peek() {
16038                    Token::Comma => {
16039                        self.advance();
16040                    }
16041                    Token::RParen => {
16042                        self.advance();
16043                        break;
16044                    }
16045                    other => {
16046                        return Err(self.err(format!(
16047                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16048                        )));
16049                    }
16050                }
16051            }
16052            cols
16053        } else {
16054            Vec::new()
16055        };
16056        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16057        // storage parameters. pgvector emits `WITH (lists = N)` for
16058        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16059        // SPG's HNSW picks its own parameters today (tunable via
16060        // env vars), so the WITH clause is informational and dropped.
16061        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16062            self.advance();
16063            if !matches!(self.peek(), Token::LParen) {
16064                return Err(self.err(format!(
16065                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16066                    self.peek()
16067                )));
16068            }
16069            self.advance();
16070            loop {
16071                if matches!(self.peek(), Token::RParen) {
16072                    self.advance();
16073                    break;
16074                }
16075                // Drain `key = value` or bare `key` tokens.
16076                let _ = self.advance(); // key
16077                if matches!(self.peek(), Token::Eq) {
16078                    self.advance();
16079                    let _ = self.advance(); // value (int / string / ident)
16080                }
16081                match self.peek() {
16082                    Token::Comma => {
16083                        self.advance();
16084                    }
16085                    Token::RParen => {
16086                        self.advance();
16087                        break;
16088                    }
16089                    other => {
16090                        return Err(self.err(format!(
16091                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16092                        )));
16093                    }
16094                }
16095            }
16096        }
16097        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16098        // which sits between the key list and the WHERE clause.
16099        let mut nulls_not_distinct = false;
16100        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16101            let n1 = self.tokens.get(self.pos + 1);
16102            let n2 = self.tokens.get(self.pos + 2);
16103            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16104                self.advance(); // NULLS
16105                self.advance(); // NOT
16106                self.advance(); // DISTINCT
16107                nulls_not_distinct = true;
16108            } else if matches!(n1, Some(Token::Distinct)) {
16109                self.advance(); // NULLS
16110                self.advance(); // DISTINCT
16111            }
16112        }
16113        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16114        let partial_predicate = if matches!(self.peek(), Token::Where) {
16115            self.advance();
16116            Some(self.parse_expr(0)?)
16117        } else {
16118            None
16119        };
16120        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16121        // sense: uniqueness over an ANN structure has no clean
16122        // semantics. Reject early. (BRIN UNIQUE is similarly
16123        // meaningless — block both.)
16124        if is_unique && !matches!(method, IndexMethod::BTree) {
16125            return Err(self.err(alloc::format!(
16126                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16127                method
16128            )));
16129        }
16130        Ok(Statement::CreateIndex(CreateIndexStatement {
16131            concurrently,
16132            name,
16133            key_order,
16134            key_collation,
16135            table,
16136            column,
16137            nulls_not_distinct,
16138            method,
16139            if_not_exists,
16140            included_columns,
16141            partial_predicate,
16142            extra_columns: extra_columns.clone(),
16143            expression,
16144            is_unique,
16145            opclass,
16146            method_name,
16147        }))
16148    }
16149
16150    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16151    /// column-level `REFERENCES ...` clause. The trailing FK is
16152    /// normalised into table-level shape (single-element columns +
16153    /// parent_columns) so the engine sees one uniform constraint list.
16154    fn parse_column_def_with_fk(
16155        &mut self,
16156    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16157        let col = self.parse_column_def()?;
16158        // v7.39 (round 308, V29) — an explicitly named inline FK:
16159        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16160        // loop leaves this spelling intact precisely so the name can be
16161        // kept here; PG reports it in violation messages and matches it
16162        // in `SET CONSTRAINTS`.
16163        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16164        {
16165            self.advance();
16166            Some(self.expect_ident_like()?)
16167        } else {
16168            None
16169        };
16170        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16171        let inline_references = matches!(
16172            self.peek(),
16173            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16174        );
16175        if !inline_references {
16176            return Ok((col, None));
16177        }
16178        let (
16179            parent_table,
16180            parent_columns,
16181            on_delete,
16182            on_update,
16183            match_type,
16184            deferrable,
16185            initially_deferred,
16186        ) = self.parse_references_tail(1)?;
16187        let fk = ForeignKeyConstraint {
16188            name: declared_name,
16189            columns: vec![col.name.clone()],
16190            parent_table,
16191            parent_columns,
16192            on_delete,
16193            on_update,
16194            match_type,
16195            deferrable,
16196            initially_deferred,
16197        };
16198        Ok((col, Some(fk)))
16199    }
16200
16201    /// v7.13.0 — parse a column type (consuming the type ident and
16202    /// any trailing parameters / `[]`), without surrounding column
16203    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16204    /// Returns the resolved `ColumnTypeName` plus implied
16205    /// `(auto_increment, not_null)` flags from PG SERIAL family
16206    /// shorthands — callers that don't expect those (ALTER COLUMN
16207    /// TYPE) can discard them.
16208    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16209        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16210        Ok(ty)
16211    }
16212
16213    #[allow(clippy::type_complexity)]
16214    fn parse_type_with_implied_flags(
16215        &mut self,
16216    ) -> Result<
16217        (
16218            ColumnTypeName,
16219            bool,
16220            bool,
16221            Option<String>,
16222            Collation,
16223            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16224            bool,
16225            // v7.39 (round 676) — the collation NAME as written, which the
16226            // `Collation` enum above cannot carry.
16227            Option<String>,
16228            bool,
16229            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16230            // list captured at type-parse time. None for all
16231            // non-ENUM types.
16232            Option<Vec<String>>,
16233            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16234            // list. Distinct from ENUM (subset semantics).
16235            Option<Vec<String>>,
16236            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16237            // width, lost when the type collapses to SmallInt / Int.
16238            Option<MysqlIntWidth>,
16239            // v7.39 (round 424) — declared fractional-seconds precision of a
16240            // MySQL temporal column (bare spelling = 0). None under PG.
16241            Option<u8>,
16242        ),
16243        ParseError,
16244    > {
16245        let mut ty_ident = match self.advance() {
16246            Token::Ident(s) => s,
16247            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16248            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16249            // '<span>'` literal grammar. As a column type it lands
16250            // here directly; downstream resolution still uses the
16251            // canonical lowercase string.
16252            Token::Interval => "interval".to_string(),
16253            other => {
16254                return Err(ParseError {
16255                    message: format!("expected column type, got {other:?}"),
16256                    token_pos: self.consumed_pos(),
16257                });
16258            }
16259        };
16260        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16261        // pg_dump qualifies extension types (`public.vector(1024)`).
16262        // SPG is single-namespace; drop the schema and resolve the
16263        // bare type — same treatment table names already get.
16264        while matches!(self.peek(), Token::Dot) {
16265            self.advance();
16266            ty_ident = self.expect_ident_like()?;
16267        }
16268        let mut implied_auto_increment = false;
16269        let mut implied_not_null = false;
16270        let mut user_type_ref: Option<String> = None;
16271        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16272        // value list, captured here and bubbled up through the
16273        // ColumnDef so the engine can attach it to the column
16274        // schema (and validate INSERT cells against it).
16275        let mut inline_enum_variants: Option<Vec<String>> = None;
16276        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16277        let mut inline_set_variants: Option<Vec<String>> = None;
16278        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16279        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16280        // collapses to SmallInt / Int. Only under the MySQL dialect.
16281        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16282        // v7.39 (round 424) — the declared fractional-seconds precision of a
16283        // MySQL temporal column. Set by the temporal arms below; stays None
16284        // for PG (whose temporal columns keep full microseconds).
16285        let mut mysql_fsp: Option<u8> = None;
16286        let mut ty = match ty_ident.as_str() {
16287            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16288            "smallserial" | "serial2" => {
16289                implied_auto_increment = true;
16290                implied_not_null = true;
16291                ColumnTypeName::SmallInt
16292            }
16293            "serial" | "serial4" => {
16294                implied_auto_increment = true;
16295                implied_not_null = true;
16296                ColumnTypeName::Int
16297            }
16298            "bigserial" | "serial8" => {
16299                implied_auto_increment = true;
16300                implied_not_null = true;
16301                ColumnTypeName::BigInt
16302            }
16303            // MySQL flavours we accept by aliasing to the closest SPG
16304            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16305            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16306            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16307            // without semantic effect.
16308            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16309            // PG's internal type names; pg_dump and hand-written PG schemas
16310            // use them interchangeably with smallint / int / bigint (the cast
16311            // path already accepted them, only the column grammar didn't).
16312            "smallint" | "int2" => {
16313                // v7.14.0 — MySQL display-width on integers
16314                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16315                // parenthesised number is purely cosmetic — it
16316                // doesn't change storage. Accept + discard.
16317                self.consume_optional_paren_size();
16318                ColumnTypeName::SmallInt
16319            }
16320            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16321            // canonical encoding for BOOLEAN. Every MySQL driver
16322            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16323            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16324            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16325            // gave the customer i16-shaped values where the app
16326            // expected bool — a Tier-A silent type drift on
16327            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16328            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16329            // stay SmallInt (the legacy width-agnostic path).
16330            "tinyint" => {
16331                let width = self.peek_optional_paren_size_value();
16332                self.consume_optional_paren_size();
16333                if width == Some(1) {
16334                    ColumnTypeName::Bool
16335                } else {
16336                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16337                    // lost width so the write path can enforce -128..127.
16338                    if self.mysql_dialect {
16339                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16340                    }
16341                    ColumnTypeName::SmallInt
16342                }
16343            }
16344            "mediumint" => {
16345                self.consume_optional_paren_size();
16346                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16347                if self.mysql_dialect {
16348                    mysql_int_width = Some(MysqlIntWidth::Medium);
16349                }
16350                ColumnTypeName::Int
16351            }
16352            "int" | "integer" | "int4" => {
16353                self.consume_optional_paren_size();
16354                ColumnTypeName::Int
16355            }
16356            "bigint" | "int8" => {
16357                self.consume_optional_paren_size();
16358                ColumnTypeName::BigInt
16359            }
16360            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16361            // (mailrs round-5 G6). Consume the optional `PRECISION`
16362            // tail when the type keyword was `double` / `DOUBLE`.
16363            //
16364            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16365            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16366            // p in 1..=24 is real, 25..=53 is double precision, and
16367            // anything else is an error.
16368            "float" | "double" | "real" => {
16369                if ty_ident.eq_ignore_ascii_case("double")
16370                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16371                {
16372                    self.advance();
16373                }
16374                if ty_ident.eq_ignore_ascii_case("real") {
16375                    // v7.39 (round 274) — the two dialects genuinely
16376                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16377                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16378                    // 32-bit globally and thereby narrowed the stored
16379                    // precision of every MySQL REAL column.
16380                    if self.mysql_dialect {
16381                        ColumnTypeName::Float
16382                    } else {
16383                        ColumnTypeName::Real
16384                    }
16385                } else if ty_ident.eq_ignore_ascii_case("float")
16386                    && self.mysql_dialect
16387                    && matches!(self.peek(), Token::LParen)
16388                    && self.peek_paren_has_comma()
16389                {
16390                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16391                    // display form (`FLOAT(10,2)`), which PG has no
16392                    // equivalent of. It was `syntax error at or near ","`,
16393                    // so the whole CREATE failed. The digits are a display
16394                    // hint only; SPG stores the full double.
16395                    self.consume_optional_paren_size();
16396                    ColumnTypeName::Float
16397                } else if ty_ident.eq_ignore_ascii_case("float")
16398                    && matches!(self.peek(), Token::LParen)
16399                {
16400                    // PG words the two bounds differently, and
16401                    // parse_paren_size already rejects a zero.
16402                    let p = self.parse_paren_size("FLOAT")?;
16403                    if p > 53 {
16404                        return Err(self.err(String::from(
16405                            "precision for type float must be less than 54 bits",
16406                        )));
16407                    }
16408                    if p <= 24 {
16409                        ColumnTypeName::Real
16410                    } else {
16411                        ColumnTypeName::Float
16412                    }
16413                } else {
16414                    ColumnTypeName::Float
16415                }
16416            }
16417            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16418            "float4" => ColumnTypeName::Real,
16419            "float8" => ColumnTypeName::Float,
16420            "text" => ColumnTypeName::Text,
16421            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16422            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16423            // real MySQL schema and NONE of them existed: the CREATE
16424            // failed outright with `type "blob" does not exist`, so the
16425            // table was never made. The sizes differ only in MySQL's
16426            // maximum length, which SPG does not cap, so they collapse
16427            // onto TEXT and BYTEA the way the unsized spellings do.
16428            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16429            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16430            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16431            // enforce, consumed so the declaration parses.
16432            "varbinary" | "binary" => {
16433                self.consume_optional_paren_size();
16434                ColumnTypeName::Bytes
16435            }
16436            "name" => ColumnTypeName::Name,
16437            "xid" => ColumnTypeName::Xid,
16438            "oid" => ColumnTypeName::Oid,
16439            "xid8" => ColumnTypeName::Xid8,
16440            "bool" | "boolean" => ColumnTypeName::Bool,
16441            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16442            // an unbounded `character varying`, which the arm below has always
16443            // read as text. Only the short spelling demanded a length, so
16444            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16445            // there is — failed on `VARCHAR type requires (N)` while the long
16446            // spelling of the same thing was accepted. The same asymmetry
16447            // round 613 closed on the CAST side, here on the DDL side.
16448            "varchar" => {
16449                if matches!(self.peek(), Token::LParen) {
16450                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16451                } else {
16452                    ColumnTypeName::Text
16453                }
16454            }
16455            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16456            // `character` below (SQL standard).
16457            "char" => {
16458                if matches!(self.peek(), Token::LParen) {
16459                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16460                } else {
16461                    ColumnTypeName::Char(1)
16462                }
16463            }
16464            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16465            // `character(n)` = char, bare `character` = char(1). Unbounded
16466            // `character varying` maps to text.
16467            "character" => {
16468                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16469                    self.advance();
16470                    if matches!(self.peek(), Token::LParen) {
16471                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16472                    } else {
16473                        ColumnTypeName::Text
16474                    }
16475                } else if matches!(self.peek(), Token::LParen) {
16476                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16477                } else {
16478                    ColumnTypeName::Char(1)
16479                }
16480            }
16481            "vector" => {
16482                let dim = self.parse_paren_size("VECTOR")?;
16483                let encoding = self.parse_optional_vector_encoding()?;
16484                ColumnTypeName::Vector { dim, encoding }
16485            }
16486            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16487            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16488            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16489            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16490            // DECIMAL(10,2))` — how nearly every money column is written,
16491            // in either dialect — was a syntax error and the table was
16492            // never created. `FIXED` is MySQL's alias alone, so it is
16493            // taken only in that dialect.
16494            "numeric" | "decimal" | "dec" => {
16495                let (precision, scale) = self.parse_optional_numeric_params()?;
16496                ColumnTypeName::Numeric(precision, scale)
16497            }
16498            "fixed" if self.mysql_dialect => {
16499                let (precision, scale) = self.parse_optional_numeric_params()?;
16500                ColumnTypeName::Numeric(precision, scale)
16501            }
16502            "date" => ColumnTypeName::Date,
16503            // MySQL's `DATETIME` is the same domain as standard
16504            // `TIMESTAMP` — accept both spellings.
16505            "timestamp" | "datetime" => {
16506                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16507                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16508                // TIME ZONE` clause, so consume it first.
16509                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16510                // (it truncates on write and pads on render), so capture it;
16511                // a bare spelling means precision 0 there. PG stores µs always
16512                // and keeps `None`.
16513                let n = self.take_optional_paren_size();
16514                if self.mysql_dialect {
16515                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16516                }
16517                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16518                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16519                // the full form. SPG canonicalises:
16520                //   - WITH TIME ZONE    → Timestamptz
16521                //   - WITHOUT TIME ZONE → Timestamp
16522                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16523                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16524                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16525                {
16526                    self.advance(); // WITH
16527                    self.advance(); // TIME
16528                    self.advance(); // ZONE
16529                    ColumnTypeName::Timestamptz
16530                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16531                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16532                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16533                {
16534                    self.advance(); // WITHOUT
16535                    self.advance(); // TIME
16536                    self.advance(); // ZONE
16537                    ColumnTypeName::Timestamp
16538                } else {
16539                    // A second `(precision)` cannot legally follow, but the
16540                    // old grammar tolerated it; keep that tolerance.
16541                    self.consume_optional_paren_size();
16542                    ColumnTypeName::Timestamp
16543                }
16544            }
16545            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16546            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16547            // only PG-wire OID differs.
16548            "timestamptz" => {
16549                self.consume_optional_paren_size();
16550                ColumnTypeName::Timestamptz
16551            }
16552            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16553            // validation. We accept the JSONB spelling too because
16554            // most PG clients default to it; SPG doesn't distinguish
16555            // the two (no path-operator perf advantage to model).
16556            "json" => ColumnTypeName::Json,
16557            "jsonb" => ColumnTypeName::Jsonb,
16558            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16559            // surface here. Same storage shape; mapping happens at
16560            // the engine side via the ColumnTypeName → DataType
16561            // resolver. Literal forms are handled at coerce_value
16562            // time so the lexer stays untouched.
16563            "bytea" | "bytes" => ColumnTypeName::Bytes,
16564            // v7.17.0 Phase 7 — PG network address types
16565            // v7.17.0 had a Text-backed fallback here for
16566            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16567            // each to a first-class type; the keywords are
16568            // bound below in the ζ-A block.
16569            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16570            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16571            // arrives in v7.12.1+; the type itself loads here so
16572            // mailrs's `scripts/init-schema.sql` runs unmodified.
16573            "tsvector" => ColumnTypeName::TsVector,
16574            "tsquery" => ColumnTypeName::TsQuery,
16575            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16576            // surface for Django / Rails / Hibernate's default
16577            // PK pattern.
16578            "uuid" => ColumnTypeName::Uuid,
16579            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16580            // Storage = three-field {months, days, micros}, catalog
16581            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16582            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16583            "interval" => {
16584                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16585                // SECOND` and an optional `(p)` precision. SPG stores the full
16586                // {months,days,micros}; consume + ignore the qualifier/precision.
16587                while matches!(self.peek(), Token::To)
16588                    || matches!(self.peek(), Token::Ident(s) if matches!(
16589                        s.to_ascii_lowercase().as_str(),
16590                        "year" | "month" | "day" | "hour" | "minute" | "second"
16591                    ))
16592                {
16593                    self.advance();
16594                }
16595                self.consume_optional_paren_size();
16596                ColumnTypeName::Interval
16597            }
16598            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16599            // i64 microseconds since 00:00:00. Wire OID 1083.
16600            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16601            "time" => {
16602                // v7.39 (round 424) — MySQL TIME carries a semantic
16603                // fractional-seconds precision, bare meaning 0.
16604                let n = self.take_optional_paren_size();
16605                if self.mysql_dialect {
16606                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16607                }
16608                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16609                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16610                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16611                {
16612                    self.advance();
16613                    self.advance();
16614                    self.advance();
16615                    ColumnTypeName::TimeTz
16616                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16617                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16618                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16619                {
16620                    self.advance();
16621                    self.advance();
16622                    self.advance();
16623                    ColumnTypeName::Time
16624                } else {
16625                    ColumnTypeName::Time
16626                }
16627            }
16628            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16629            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16630            "year" => ColumnTypeName::Year,
16631            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16632            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16633            "timetz" => ColumnTypeName::TimeTz,
16634            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16635            // Wire OID 790.
16636            "money" => ColumnTypeName::Money,
16637            // v7.17.0 Phase 3.P0-38 — PG range types.
16638            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16639            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16640            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16641            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16642            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16643            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16644            // v7.37.5 δ — PG 14+ multirange keywords.
16645            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16646            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16647            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16648            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16649            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16650            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16651            // v7.37.5 ε — PG geometry scalar keywords.
16652            "point" => ColumnTypeName::Point,
16653            "lseg" => ColumnTypeName::Lseg,
16654            "path" => ColumnTypeName::Path,
16655            "box" => ColumnTypeName::PgBox,
16656            "polygon" => ColumnTypeName::Polygon,
16657            "line" => ColumnTypeName::Line,
16658            "circle" => ColumnTypeName::Circle,
16659            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16660            "inet" => ColumnTypeName::Inet,
16661            "cidr" => ColumnTypeName::Cidr,
16662            "macaddr" => ColumnTypeName::Macaddr,
16663            "macaddr8" => ColumnTypeName::Macaddr8,
16664            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16665            // width in the value, so the optional `(N)` typmod is accepted and
16666            // ignored (the column stores whatever width it's given).
16667            "bit" => {
16668                let varying = matches!(
16669                    self.peek(),
16670                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16671                );
16672                if varying {
16673                    self.advance();
16674                }
16675                // v7.39 (round 281) — the length used to be parsed and
16676                // dropped, so `bit(3)` accepted a five-bit string.
16677                let n = if matches!(self.peek(), Token::LParen) {
16678                    self.parse_paren_size("BIT")?
16679                } else {
16680                    0
16681                };
16682                if varying {
16683                    ColumnTypeName::BitVarying(n)
16684                } else {
16685                    ColumnTypeName::Bit(n)
16686                }
16687            }
16688            "varbit" => {
16689                let n = if matches!(self.peek(), Token::LParen) {
16690                    self.parse_paren_size("VARBIT")?
16691                } else {
16692                    0
16693                };
16694                ColumnTypeName::BitVarying(n)
16695            }
16696            "xml" => ColumnTypeName::Xml,
16697            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16698            "hstore" => ColumnTypeName::Hstore,
16699            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16700            // `ENUM('a','b','c')`. Storage is TEXT; the value
16701            // list lands on `inline_enum_variants` for the
16702            // engine to validate INSERT cells against. Empty
16703            // value list is a parse error (matches MySQL).
16704            "enum" => {
16705                // Expect the opening `(`.
16706                if !matches!(self.peek(), Token::LParen) {
16707                    return Err(self.err(alloc::format!(
16708                        "expected '(' after ENUM, got {:?}",
16709                        self.peek()
16710                    )));
16711                }
16712                self.advance();
16713                let mut variants: Vec<String> = Vec::new();
16714                loop {
16715                    match self.advance() {
16716                        Token::String(s) => variants.push(s),
16717                        other => {
16718                            return Err(self.err(alloc::format!(
16719                                "ENUM(...) expects string literal variants, got {other:?}"
16720                            )));
16721                        }
16722                    }
16723                    match self.peek() {
16724                        Token::Comma => {
16725                            self.advance();
16726                            continue;
16727                        }
16728                        Token::RParen => {
16729                            self.advance();
16730                            break;
16731                        }
16732                        other => {
16733                            return Err(self.err(alloc::format!(
16734                                "expected ',' or ')' in ENUM(...), got {other:?}"
16735                            )));
16736                        }
16737                    }
16738                }
16739                if variants.is_empty() {
16740                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16741                }
16742                inline_enum_variants = Some(variants);
16743                // Storage is plain TEXT; the variant list lives on
16744                // the ColumnSchema side.
16745                ColumnTypeName::Text
16746            }
16747            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16748            // `SET('a','b','c')`. Same parse shape as ENUM;
16749            // semantics differ (subset rather than pick-one).
16750            "set" => {
16751                if !matches!(self.peek(), Token::LParen) {
16752                    return Err(self.err(alloc::format!(
16753                        "expected '(' after SET, got {:?}",
16754                        self.peek()
16755                    )));
16756                }
16757                self.advance();
16758                let mut variants: Vec<String> = Vec::new();
16759                loop {
16760                    match self.advance() {
16761                        Token::String(s) => variants.push(s),
16762                        other => {
16763                            return Err(self.err(alloc::format!(
16764                                "SET(...) expects string literal variants, got {other:?}"
16765                            )));
16766                        }
16767                    }
16768                    match self.peek() {
16769                        Token::Comma => {
16770                            self.advance();
16771                            continue;
16772                        }
16773                        Token::RParen => {
16774                            self.advance();
16775                            break;
16776                        }
16777                        other => {
16778                            return Err(self.err(alloc::format!(
16779                                "expected ',' or ')' in SET(...), got {other:?}"
16780                            )));
16781                        }
16782                    }
16783                }
16784                if variants.is_empty() {
16785                    return Err(self.err("SET(...) must declare at least one variant".into()));
16786                }
16787                inline_set_variants = Some(variants);
16788                ColumnTypeName::Text
16789            }
16790            _other => {
16791                // v7.17.0 Phase 1.4 — unknown ident → defer
16792                // resolution to the engine. Stored as Text in
16793                // ColumnTypeName + the original name carried as
16794                // `user_type_ref` so CREATE TABLE can look up
16795                // user-defined enum / domain types.
16796                user_type_ref = Some(ty_ident.clone());
16797                ColumnTypeName::Text
16798            }
16799        };
16800        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16801        // right after the type keyword. Pre-4.4 SPG consumed +
16802        // discarded the keyword, leaving a customer column
16803        // declared `id INT UNSIGNED NOT NULL` silently accepting
16804        // negative values — a Tier-A correctness drift where
16805        // application invariants (auto-increment-IDs never
16806        // negative) silently broke on cutover. Now: capture as
16807        // a column flag, persist on the schema, enforce at
16808        // INSERT / UPDATE time.
16809        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16810        {
16811            self.advance();
16812            true
16813        } else {
16814            false
16815        };
16816        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16817        // `<type> COLLATE <name>` post-fixes on text columns. SPG
16818        // stores text as UTF-8 always so CHARACTER SET is still a
16819        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16820        // name: it gets classified into a `Collation` variant the
16821        // engine consults at WHERE-eval time. PG `default` /
16822        // `pg_catalog.default` / `C` / `POSIX` collations all
16823        // resolve to `Binary` (the prior behaviour); `_ci` /
16824        // `case_insensitive` / `nocase` shift to CaseInsensitive.
16825        // The schema-qualifier form (`pg_catalog.default`) lexes
16826        // as `Ident '.' Ident` — peek for the `.` and consume both
16827        // halves so it's treated as one collation name. PG's
16828        // `IDENT.IDENT` collation form (which can appear here) is
16829        // resolved by Collation::from_collation_name on the bare
16830        // identifier after the dot.
16831        let mut collation = Collation::Binary;
16832        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16833        // clause was written. The engine needs this to tell an explicit
16834        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16835        // clause at all: both resolve to `Collation::Binary`, but under the
16836        // MySQL dialect the latter takes the folding default collation.
16837        let mut collation_explicit = false;
16838        let mut collation_name: Option<alloc::string::String> = None;
16839        loop {
16840            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16841                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16842            {
16843                self.advance(); // CHARACTER
16844                self.advance(); // SET
16845                if matches!(
16846                    self.peek(),
16847                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16848                ) {
16849                    self.advance();
16850                }
16851                continue;
16852            }
16853            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16854                self.advance(); // COLLATE
16855                // Accept Ident / QuotedIdent / String AND the
16856                // keyword-tokenised `Default` (PG `pg_catalog.default`
16857                // and bare `DEFAULT` collation names — `default` is a
16858                // reserved word so the lexer hands back Token::Default
16859                // not Token::Ident).
16860                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16861                    match this.peek().clone() {
16862                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16863                            this.advance();
16864                            Some(s)
16865                        }
16866                        Token::Default => {
16867                            this.advance();
16868                            Some(alloc::string::String::from("default"))
16869                        }
16870                        _ => None,
16871                    }
16872                };
16873                let raw = if let Some(head) = read_collation_atom(self) {
16874                    // Schema-qualified PG form: `pg_catalog.default`.
16875                    if matches!(self.peek(), Token::Dot) {
16876                        self.advance();
16877                        let tail = read_collation_atom(self).unwrap_or_default();
16878                        alloc::format!("{head}.{tail}")
16879                    } else {
16880                        head
16881                    }
16882                } else {
16883                    alloc::string::String::new()
16884                };
16885                if !raw.is_empty() {
16886                    collation_explicit = true;
16887                    // v7.39 (round 676) — keep the name too. The enum below
16888                    // folds C / POSIX / en_US / default into one value, and
16889                    // `pg_attribute.attcollation` has to tell them apart.
16890                    // The schema qualifier goes: PG's `pg_catalog.default`
16891                    // and a bare `default` name the same collation.
16892                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16893                    // encoding suffix. Round 676 used `rsplit('.')` for
16894                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16895                    // PG writes `pg_catalog.default` (qualifier) and
16896                    // `en_US.utf8` (locale + encoding) with the same
16897                    // separator. Only `pg_catalog.` is a qualifier, and it
16898                    // is the only one PG's own dumps emit.
16899                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16900                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16901                    collation_name = Some(alloc::string::String::from(bare));
16902                    let parsed = Collation::from_collation_name(&raw);
16903                    // Last COLLATE clause wins, but `Binary` from a
16904                    // bare keyword like `default` should not
16905                    // silently downgrade a stronger one set earlier
16906                    // on the same column. v7.17 only ships one
16907                    // non-Binary variant so a simple OR is enough.
16908                    if parsed != Collation::Binary {
16909                        collation = parsed;
16910                    }
16911                }
16912                continue;
16913            }
16914            break;
16915        }
16916        // v7.10.10 — postfix `[]` widens the base type to its array
16917        // type. PG accepts `TYPE[]` after any base type and so does
16918        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16919        // all through; the old "only TEXT[]" note was stale).
16920        if matches!(self.peek(), Token::LBracket) {
16921            self.advance();
16922            if !matches!(self.peek(), Token::RBracket) {
16923                return Err(self.err(alloc::format!(
16924                    "TEXT[] takes no dimension; got {:?}",
16925                    self.peek()
16926                )));
16927            }
16928            self.advance();
16929            // v7.11.13 — widened to INT[] and BIGINT[] in addition
16930            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16931            // still error here.
16932            ty = match ty {
16933                ColumnTypeName::Text => ColumnTypeName::TextArray,
16934                ColumnTypeName::Int => ColumnTypeName::IntArray,
16935                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16936                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16937                // `[]` grammar. Wire OID 1187.
16938                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16939                // v7.37.5 γ — full PG array-of-scalar family.
16940                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16941                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16942                ColumnTypeName::Float => ColumnTypeName::FloatArray,
16943                // NUMERIC(p, s) loses its precision params at the
16944                // array level (matches PG: `NUMERIC[]` is untyped,
16945                // per-element precision flows through values).
16946                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16947                ColumnTypeName::Date => ColumnTypeName::DateArray,
16948                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16949                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16950                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16951                ColumnTypeName::Json => ColumnTypeName::JsonArray,
16952                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16953                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16954                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16955                // the array level (matches PG semantics where the
16956                // element precision is per-row, not column-wide).
16957                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16958                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16959                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16960                // follow-up.
16961                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16962                other => {
16963                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16964                }
16965            };
16966            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16967            // for INT/TEXT/BIGINT. Anything else is an error.
16968            if matches!(self.peek(), Token::LBracket) {
16969                self.advance();
16970                if !matches!(self.peek(), Token::RBracket) {
16971                    return Err(self.err(alloc::format!(
16972                        "TYPE[][] second dimension takes no size; got {:?}",
16973                        self.peek()
16974                    )));
16975                }
16976                self.advance();
16977                ty = match ty {
16978                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16979                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16980                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16981                    // v7.39 (read01 round 75) — bool[][].
16982                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16983                    other => {
16984                        return Err(self.err(alloc::format!(
16985                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16986                             TEXT[][] only; got {other:?}"
16987                        )));
16988                    }
16989                };
16990            }
16991        }
16992        Ok((
16993            ty,
16994            implied_auto_increment,
16995            implied_not_null,
16996            user_type_ref,
16997            collation,
16998            collation_explicit,
16999            collation_name,
17000            is_unsigned,
17001            inline_enum_variants,
17002            inline_set_variants,
17003            mysql_int_width,
17004            mysql_fsp,
17005        ))
17006    }
17007
17008    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17009        // v7.20 — PG reserves the table-constraint keywords, so a
17010        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17011        // malformed constraint clause (e.g. `UNIQUE a` missing its
17012        // parens), not a column named "unique". Since v7.17's
17013        // unknown-type leniency (`user_type_ref`) such a clause
17014        // would otherwise parse as a column with a user-defined
17015        // type — silently accepting invalid DDL. Quoted
17016        // identifiers ("unique" / `unique`) remain valid names.
17017        if let Token::Ident(s) = self.peek()
17018            && [
17019                "unique",
17020                "primary",
17021                "foreign",
17022                "constraint",
17023                "check",
17024                "references",
17025                "exclude",
17026            ]
17027            .iter()
17028            .any(|kw| s.eq_ignore_ascii_case(kw))
17029        {
17030            return Err(self.err(alloc::format!(
17031                "unexpected reserved keyword '{s}' at start of column definition \
17032                 (malformed table constraint?)"
17033            )));
17034        }
17035        let name = self.expect_ident_like()?;
17036        let (
17037            ty,
17038            implied_auto_increment,
17039            implied_not_null,
17040            user_type_ref,
17041            collation,
17042            collation_explicit,
17043            collation_name,
17044            is_unsigned,
17045            inline_enum_variants,
17046            inline_set_variants,
17047            mysql_int_width,
17048            mysql_fsp,
17049        ) = self.parse_type_with_implied_flags()?;
17050        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17051        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17052        // each at most once.
17053        let mut default: Option<Expr> = None;
17054        let mut nullable = !implied_not_null;
17055        let mut nullability_seen = implied_not_null;
17056        let mut auto_increment = implied_auto_increment;
17057        let mut is_primary_key = false;
17058        let mut is_unique = false;
17059        let mut unique_nulls_not_distinct = false;
17060        let mut constraint_deferrable = false;
17061        let mut constraint_initially_deferred = false;
17062        let mut check: Option<Expr> = None;
17063        let mut on_update_runtime: Option<Expr> = None;
17064        let mut generated_stored_expr: Option<Box<Expr>> = None;
17065        let mut identity_always = false;
17066        loop {
17067            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17068            // not-null constraints by name and pg_dump emits them
17069            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17070            // NOT NULL`. Accept and discard the name; whatever
17071            // constraint follows is parsed by the arms below.
17072            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17073                // v7.39 (round 308, V29) — a name on an inline
17074                // REFERENCES belongs to the FOREIGN KEY, and the caller
17075                // (`parse_column_def_with_fk`) is what builds it, so
17076                // leave the whole clause for it. Dropping the name here
17077                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17078                // as the synthesised `c_pid_fkey` — which then could
17079                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17080                // `advance()` takes tokens by `mem::replace`, so there
17081                // is no rewinding once consumed.
17082                if matches!(
17083                    self.tokens.get(self.pos + 2),
17084                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17085                ) {
17086                    break;
17087                }
17088                self.advance();
17089                let _name = self.expect_ident_like()?;
17090                continue;
17091            }
17092            // v7.39 (round 379) — MySQL's SHORT generated-column form
17093            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17094            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17095            // below), but hand-written schemas and app migrations use this.
17096            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17097            // SPG computes-and-stores either way, like the long form.
17098            if matches!(self.peek(), Token::As) {
17099                self.advance();
17100                if !matches!(self.peek(), Token::LParen) {
17101                    return Err(self.err(alloc::format!(
17102                        "expected '(' after AS in a generated column, got {:?}",
17103                        self.peek()
17104                    )));
17105                }
17106                self.advance();
17107                let expr = self.parse_expr(0)?;
17108                if !matches!(self.peek(), Token::RParen) {
17109                    return Err(self.err(alloc::format!(
17110                        "expected ')' after AS (<expr>), got {:?}",
17111                        self.peek()
17112                    )));
17113                }
17114                self.advance();
17115                if matches!(self.peek(), Token::Ident(s)
17116                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17117                {
17118                    self.advance();
17119                }
17120                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17121                continue;
17122            }
17123            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17124            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17125            // the modern replacement for SERIAL in hand-written
17126            // schemas). Both flavours map onto the auto-increment
17127            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17128            // ALWAYS's reject-explicit-values nuance is documented
17129            // leniency. Generated EXPRESSION columns
17130            // (`AS (expr) STORED`) are not supported: error loudly
17131            // instead of silently storing NULLs.
17132            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17133                self.advance();
17134                let mut saw_generated_always = false;
17135                match self.peek().clone() {
17136                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17137                        self.advance();
17138                        saw_generated_always = true;
17139                    }
17140                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17141                        self.advance();
17142                        if !matches!(self.peek(), Token::Default) {
17143                            return Err(self.err(alloc::format!(
17144                                "expected DEFAULT after GENERATED BY, got {:?}",
17145                                self.peek()
17146                            )));
17147                        }
17148                        self.advance();
17149                    }
17150                    other => {
17151                        return Err(self.err(alloc::format!(
17152                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17153                        )));
17154                    }
17155                }
17156                if !matches!(self.peek(), Token::As) {
17157                    return Err(self.err(alloc::format!(
17158                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17159                        self.peek()
17160                    )));
17161                }
17162                self.advance();
17163                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17164                // ( <expr> ) STORED` stored computed-column. The
17165                // expression is captured for the engine to recompute
17166                // on every INSERT / UPDATE. v7.37.7 accepts the
17167                // STORED keyword only; PG also has VIRTUAL, which
17168                // v7.37.7 carves out (sentori only uses STORED).
17169                if matches!(self.peek(), Token::LParen) {
17170                    self.advance();
17171                    let expr = self.parse_expr(0)?;
17172                    if !matches!(self.peek(), Token::RParen) {
17173                        return Err(self.err(alloc::format!(
17174                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17175                            self.peek()
17176                        )));
17177                    }
17178                    self.advance();
17179                    let stored = match self.peek() {
17180                        Token::Ident(s) | Token::QuotedIdent(s)
17181                            if s.eq_ignore_ascii_case("stored") =>
17182                        {
17183                            self.advance();
17184                            true
17185                        }
17186                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17187                        // generated columns. SPG computes them on write and
17188                        // persists like STORED; the two are observably
17189                        // identical for query results (the value, recompute
17190                        // on base-column change, and NOT NULL enforcement all
17191                        // match), so a PG 18 schema/dump using VIRTUAL loads
17192                        // and behaves correctly. The compute-on-read storage
17193                        // saving is an invisible internal difference.
17194                        Token::Ident(s) | Token::QuotedIdent(s)
17195                            if s.eq_ignore_ascii_case("virtual") =>
17196                        {
17197                            self.advance();
17198                            false
17199                        }
17200                        other => {
17201                            return Err(self.err(alloc::format!(
17202                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17203                                 got {other:?}"
17204                            )));
17205                        }
17206                    };
17207                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17208                    generated_stored_expr = Some(Box::new(expr));
17209                    continue;
17210                }
17211                self.expect_keyword_ident("identity")?;
17212                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17213                // consume the balanced parens and discard (SPG's
17214                // auto-increment is max+1-scan based).
17215                if matches!(self.peek(), Token::LParen) {
17216                    let mut depth = 0usize;
17217                    loop {
17218                        match self.advance() {
17219                            Token::LParen => depth += 1,
17220                            Token::RParen => {
17221                                depth -= 1;
17222                                if depth == 0 {
17223                                    break;
17224                                }
17225                            }
17226                            Token::Eof => {
17227                                return Err(self.err(
17228                                    "unterminated sequence-options parens after IDENTITY".into(),
17229                                ));
17230                            }
17231                            _ => {}
17232                        }
17233                    }
17234                }
17235                auto_increment = true;
17236                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17237                // can reject explicit non-DEFAULT INSERT values (unless
17238                // OVERRIDING SYSTEM VALUE) the way PG does.
17239                identity_always = saw_generated_always;
17240                // PG identity columns are implicitly NOT NULL.
17241                nullable = false;
17242                continue;
17243            }
17244            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17245            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17246            // is accepted today. The "ON" token is an Ident
17247            // (not reserved) — peek before consuming.
17248            if matches!(self.peek(), Token::On)
17249                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17250            {
17251                self.advance(); // ON
17252                self.advance(); // update
17253                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17254                let next = self.peek().clone();
17255                match next {
17256                    Token::Ident(s) | Token::QuotedIdent(s)
17257                        if s.eq_ignore_ascii_case("current_timestamp") =>
17258                    {
17259                        self.advance();
17260                        // Optional `(N)` precision.
17261                        if matches!(self.peek(), Token::LParen) {
17262                            self.advance();
17263                            if !matches!(self.peek(), Token::Integer(_)) {
17264                                return Err(self.err(alloc::format!(
17265                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17266                                    self.peek()
17267                                )));
17268                            }
17269                            self.advance();
17270                            if !matches!(self.peek(), Token::RParen) {
17271                                return Err(self.err(alloc::format!(
17272                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17273                                    self.peek()
17274                                )));
17275                            }
17276                            self.advance();
17277                        }
17278                        on_update_runtime = Some(Expr::FunctionCall {
17279                            name: "now".into(),
17280                            args: Vec::new(),
17281                        });
17282                        continue;
17283                    }
17284                    other => {
17285                        return Err(self.err(alloc::format!(
17286                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17287                        )));
17288                    }
17289                }
17290            }
17291            if matches!(self.peek(), Token::Default) {
17292                if default.is_some() {
17293                    return Err(self.err("DEFAULT specified twice".into()));
17294                }
17295                self.advance();
17296                default = Some(self.parse_expr(0)?);
17297                continue;
17298            }
17299            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17300            // token with NOT NULL and sits EARLIER in the loop than the
17301            // deferrability arm, so without the lookahead it was reported as
17302            // "NOT NULL specified twice" (or "expected NULL after NOT").
17303            if matches!(self.peek(), Token::Not)
17304                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17305            {
17306                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17307                self.consume_optional_deferrable_clauses()?;
17308                continue;
17309            }
17310            if matches!(self.peek(), Token::Not) {
17311                if nullability_seen {
17312                    return Err(self.err("NOT NULL specified twice".into()));
17313                }
17314                self.advance();
17315                if !matches!(self.peek(), Token::Null) {
17316                    return Err(self.err(format!(
17317                        "expected NULL after NOT in column def, got {:?}",
17318                        self.peek()
17319                    )));
17320                }
17321                self.advance();
17322                nullable = false;
17323                nullability_seen = true;
17324                continue;
17325            }
17326            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17327            // "this column is nullable" marker (the default in
17328            // standard SQL anyway). mysqldump emits it routinely
17329            // (`col TYPE NULL DEFAULT NULL` for nullable
17330            // timestamps etc). Accept + no-op.
17331            if matches!(self.peek(), Token::Null) {
17332                if nullability_seen && !nullable {
17333                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17334                    // sentence, PG18-measured (the table name is the
17335                    // caller's; the column half is exact).
17336                    return Err(self.err(alloc::format!(
17337                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17338                    )));
17339                }
17340                self.advance();
17341                nullable = true;
17342                nullability_seen = true;
17343                continue;
17344            }
17345            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17346            // arrives as a bare Ident. Match either, case-insensitive.
17347            if let Token::Ident(s) = self.peek()
17348                && (s.eq_ignore_ascii_case("auto_increment")
17349                    || s.eq_ignore_ascii_case("autoincrement"))
17350            {
17351                if auto_increment {
17352                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17353                }
17354                self.advance();
17355                auto_increment = true;
17356                continue;
17357            }
17358            // v7.9.13 — inline `PRIMARY KEY` column constraint
17359            // (mailrs F1). Implies `NOT NULL`. The engine creates
17360            // a BTree index for the PK column at CREATE TABLE time
17361            // so FK parent-side index lookups resolve.
17362            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17363            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17364            // spelling was a parse error, so a pg_dump carrying one stopped
17365            // mid-restore. The clauses are consumed by the same helper the FK
17366            // path has used since round 288 and recorded nowhere: SPG enforces
17367            // the constraint IMMEDIATELY either way, which fails earlier than
17368            // PG inside a transaction that violates-then-repairs — a refusal,
17369            // not a wrong answer. True deferral is the open remainder of F08.
17370            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17371                || (matches!(self.peek(), Token::Not)
17372                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17373            {
17374                // v7.39 (round 711) — CARRIED now (the storing half of
17375                // F08); round 621 only consumed.
17376                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17377                constraint_deferrable |= d;
17378                constraint_initially_deferred |= idef;
17379                continue;
17380            }
17381            if let Token::Ident(s) = self.peek()
17382                && s.eq_ignore_ascii_case("primary")
17383            {
17384                if is_primary_key {
17385                    return Err(self.err("PRIMARY KEY specified twice".into()));
17386                }
17387                // Peek-ahead for the required `KEY` token.
17388                let next = self.tokens.get(self.pos + 1);
17389                let next_is_key = matches!(
17390                    next,
17391                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17392                );
17393                if !next_is_key {
17394                    return Err(self.err(format!(
17395                        "expected KEY after PRIMARY in column def, got {:?}",
17396                        next
17397                    )));
17398                }
17399                self.advance(); // PRIMARY
17400                self.advance(); // KEY
17401                is_primary_key = true;
17402                if nullability_seen && nullable {
17403                    return Err(self.err(
17404                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17405                    ));
17406                }
17407                nullable = false;
17408                nullability_seen = true;
17409                continue;
17410            }
17411            // v7.13.0 — inline `UNIQUE` column constraint
17412            // (mailrs round-5 G2). Fold into a single-column
17413            // table-level UNIQUE at CREATE TABLE post-process time.
17414            if let Token::Ident(s) = self.peek()
17415                && s.eq_ignore_ascii_case("unique")
17416            {
17417                if is_unique {
17418                    return Err(self.err("UNIQUE specified twice".into()));
17419                }
17420                self.advance();
17421                is_unique = true;
17422                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17423                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17424                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17425                    let n1 = self.tokens.get(self.pos + 1);
17426                    let n2 = self.tokens.get(self.pos + 2);
17427                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17428                        self.advance(); // NULLS
17429                        self.advance(); // NOT
17430                        self.advance(); // DISTINCT
17431                        unique_nulls_not_distinct = true;
17432                    } else if matches!(n1, Some(Token::Distinct)) {
17433                        self.advance(); // NULLS
17434                        self.advance(); // DISTINCT
17435                    }
17436                }
17437                continue;
17438            }
17439            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17440            // (mailrs round-5 G3). PG semantics: column-level
17441            // CHECK is equivalent to a table-level CHECK. Multiple
17442            // inline CHECKs on the same column AND together.
17443            if let Token::Ident(s) = self.peek()
17444                && s.eq_ignore_ascii_case("check")
17445            {
17446                self.advance();
17447                if !matches!(self.peek(), Token::LParen) {
17448                    return Err(self.err(alloc::format!(
17449                        "expected '(' after CHECK in column def, got {:?}",
17450                        self.peek()
17451                    )));
17452                }
17453                self.advance();
17454                let pred = self.parse_expr(0)?;
17455                if !matches!(self.peek(), Token::RParen) {
17456                    return Err(self.err(alloc::format!(
17457                        "expected ')' to close CHECK predicate, got {:?}",
17458                        self.peek()
17459                    )));
17460                }
17461                self.advance();
17462                check = Some(match check.take() {
17463                    Some(prev) => Expr::Binary {
17464                        op: BinOp::And,
17465                        lhs: Box::new(prev),
17466                        rhs: Box::new(pred),
17467                    },
17468                    None => pred,
17469                });
17470                continue;
17471            }
17472            break;
17473        }
17474        Ok(ColumnDef {
17475            name,
17476            ty,
17477            nullable,
17478            default,
17479            auto_increment,
17480            is_primary_key,
17481            is_unique,
17482            unique_nulls_not_distinct,
17483            constraint_deferrable,
17484            constraint_initially_deferred,
17485            check,
17486            user_type_ref,
17487            on_update_runtime,
17488            collation,
17489            collation_explicit,
17490            collation_name,
17491            is_unsigned,
17492            inline_enum_variants,
17493            inline_set_variants,
17494            generated_stored_expr,
17495            identity_always,
17496            mysql_int_width,
17497            mysql_fsp,
17498        })
17499    }
17500
17501    /// `NUMERIC` may appear without parameters, with one (precision
17502    /// only, scale=0), or with both. Returns `(precision, scale)` with
17503    /// 0 = unspecified for the bare form.
17504    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17505        if !matches!(self.peek(), Token::LParen) {
17506            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17507            // we surface it as precision=0 to mean "unconstrained" so
17508            // the engine doesn't need a separate variant.
17509            return Ok((0, 0));
17510        }
17511        self.advance();
17512        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17513        // it words the out-of-range case with the value it saw. SPG
17514        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17515        // accepts failed to parse at all; values wider than i128 are
17516        // carried by the arbitrary-precision form.
17517        let precision = match self.advance() {
17518            Token::Integer(n) if (1..=1000).contains(&n) => {
17519                u16::try_from(n).expect("range-checked")
17520            }
17521            Token::Integer(n) => {
17522                return Err(ParseError {
17523                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17524                    token_pos: self.consumed_pos(),
17525                });
17526            }
17527            other => {
17528                return Err(ParseError {
17529                    message: format!(
17530                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17531                    ),
17532                    token_pos: self.consumed_pos(),
17533                });
17534            }
17535        };
17536        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17537        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17538        // then overflows). A negative scale rounds to tens / hundreds / …
17539        let scale = if matches!(self.peek(), Token::Comma) {
17540            self.advance();
17541            let neg = if matches!(self.peek(), Token::Minus) {
17542                self.advance();
17543                true
17544            } else {
17545                false
17546            };
17547            match self.advance() {
17548                Token::Integer(n) => {
17549                    let signed = if neg { -n } else { n };
17550                    if !(-1000..=1000).contains(&signed) {
17551                        return Err(ParseError {
17552                            message: format!(
17553                                "NUMERIC scale {signed} must be between -1000 and 1000"
17554                            ),
17555                            token_pos: self.consumed_pos(),
17556                        });
17557                    }
17558                    i16::try_from(signed).expect("range-checked")
17559                }
17560                other => {
17561                    return Err(ParseError {
17562                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17563                        token_pos: self.consumed_pos(),
17564                    });
17565                }
17566            }
17567        } else {
17568            0
17569        };
17570        if !matches!(self.peek(), Token::RParen) {
17571            return Err(self.err(format!(
17572                "expected ')' to close NUMERIC params, got {:?}",
17573                self.peek()
17574            )));
17575        }
17576        self.advance();
17577        Ok((precision, scale))
17578    }
17579
17580    /// Parse `(N)` where `N` is a positive integer literal — used by the
17581    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17582    /// for the error message.
17583    /// v6.0.1: parse the optional `USING <encoding>` clause that
17584    /// follows `VECTOR(N)` in a column definition. Missing clause
17585    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17586    /// ident → `ParseError` listing the encodings recognised today.
17587    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17588        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17589            return Ok(VecEncoding::F32);
17590        }
17591        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17592        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17593        // consume the token when the very next token is a known
17594        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17595        // `USING` for the caller — it's the rewrite-expression form.
17596        let n1 = self.tokens.get(self.pos + 1);
17597        let next_is_encoding = matches!(
17598            n1,
17599            Some(Token::Ident(s))
17600                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17601        );
17602        if !next_is_encoding {
17603            return Ok(VecEncoding::F32);
17604        }
17605        self.advance();
17606        let enc_ident = match self.advance() {
17607            Token::Ident(s) => s,
17608            other => {
17609                return Err(self.err(format!(
17610                    "expected vector encoding after USING, got {other:?}"
17611                )));
17612            }
17613        };
17614        match enc_ident.to_ascii_lowercase().as_str() {
17615            "sq8" => Ok(VecEncoding::Sq8),
17616            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17617            // binary16 per-element storage.
17618            "half" => Ok(VecEncoding::F16),
17619            other => Err(self.err(format!(
17620                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17621            ))),
17622        }
17623    }
17624
17625    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17626    /// without consuming it. Returns `Some(N)` when the next
17627    /// tokens are `( <int> )`; None otherwise. Used by the
17628    /// TINYINT classifier to decide whether to map to Bool or
17629    /// SmallInt.
17630    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17631        if !matches!(self.peek(), Token::LParen) {
17632            return None;
17633        }
17634        let next = self.tokens.get(self.pos + 1)?;
17635        let n = match next {
17636            Token::Integer(n) => *n,
17637            _ => return None,
17638        };
17639        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17640            return None;
17641        }
17642        Some(n)
17643    }
17644
17645    /// v7.14.0 — consume an optional MySQL display-width
17646    /// parenthesised number after an integer type, returning
17647    /// nothing. `TINYINT(1)` etc.
17648    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17649    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17650    fn peek_paren_has_comma(&self) -> bool {
17651        let mut i = self.pos + 1;
17652        let mut depth = 1usize;
17653        while depth > 0 {
17654            match self.tokens.get(i) {
17655                Some(Token::LParen) => depth += 1,
17656                Some(Token::RParen) => depth -= 1,
17657                Some(Token::Comma) if depth == 1 => return true,
17658                None | Some(Token::Eof) => return false,
17659                _ => {}
17660            }
17661            i += 1;
17662        }
17663        false
17664    }
17665
17666    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17667    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17668    /// fractional-seconds precision that drives write truncation and render
17669    /// padding, where `consume_optional_paren_size` throws it away.
17670    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17671    fn take_optional_paren_size(&mut self) -> Option<u8> {
17672        let Some(Token::Integer(n)) = self
17673            .tokens
17674            .get(self.pos + 1)
17675            .filter(|_| matches!(self.peek(), Token::LParen))
17676            .cloned()
17677        else {
17678            self.consume_optional_paren_size();
17679            return None;
17680        };
17681        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17682            self.consume_optional_paren_size();
17683            return None;
17684        }
17685        self.consume_optional_paren_size();
17686        u8::try_from(n).ok()
17687    }
17688
17689    fn consume_optional_paren_size(&mut self) {
17690        if !matches!(self.peek(), Token::LParen) {
17691            return;
17692        }
17693        self.advance();
17694        // Skip until matching RParen (allow nested or any tokens).
17695        let mut depth = 1usize;
17696        while depth > 0 {
17697            match self.peek() {
17698                Token::LParen => depth += 1,
17699                Token::RParen => depth -= 1,
17700                Token::Eof => return,
17701                _ => {}
17702            }
17703            self.advance();
17704        }
17705    }
17706
17707    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17708        if !matches!(self.peek(), Token::LParen) {
17709            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17710        }
17711        self.advance();
17712        let n = match self.advance() {
17713            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17714                message: format!("{label} size too large: {n}"),
17715                token_pos: self.consumed_pos(),
17716            })?,
17717            other => {
17718                return Err(ParseError {
17719                    message: format!("expected positive integer {label} size, got {other:?}"),
17720                    token_pos: self.consumed_pos(),
17721                });
17722            }
17723        };
17724        if !matches!(self.peek(), Token::RParen) {
17725            return Err(self.err(format!(
17726                "expected ')' after {label} size, got {:?}",
17727                self.peek()
17728            )));
17729        }
17730        self.advance();
17731        Ok(n)
17732    }
17733
17734    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17735    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17736    /// key, like MySQL) whose action skips conflicting rows.
17737    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17738    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17739    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17740    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17741    /// common bulk-upsert spellings —
17742    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17743    ///     REPLACE INTO t SELECT …
17744    /// — were a parse error / a duplicate-key failure respectively.
17745    ///
17746    /// Precedence: an explicitly written clause beats a statement-level flag.
17747    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17748    /// implicit `REPLACE` and `IGNORE` lowerings.
17749    fn parse_insert_conflict_clause(
17750        &mut self,
17751        replace: bool,
17752        ignore: bool,
17753    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17754        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17755            return Ok(Some(c));
17756        }
17757        if let Some(c) = self.parse_optional_on_conflict()? {
17758            return Ok(Some(c));
17759        }
17760        if replace {
17761            // REPLACE INTO = delete-then-insert, which PG spells as
17762            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17763            // reads an empty assignment list as "take the incoming row".
17764            return Ok(Some(crate::ast::OnConflictClause {
17765                target_columns: Vec::new(),
17766                index_where: None,
17767                constraint_name: None,
17768                mysql_lowered: true,
17769                action: crate::ast::OnConflictAction::Update {
17770                    assignments: Vec::new(),
17771                    where_: None,
17772                },
17773            }));
17774        }
17775        if ignore {
17776            return Ok(Some(Self::insert_ignore_clause()));
17777        }
17778        Ok(None)
17779    }
17780
17781    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17782    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17783    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17784    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17785    fn parse_optional_on_duplicate_key(
17786        &mut self,
17787    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17788        if !(matches!(self.peek(), Token::On)
17789            && matches!(self.tokens.get(self.pos + 1),
17790                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17791        {
17792            return Ok(None);
17793        }
17794        self.advance(); // ON
17795        self.advance(); // DUPLICATE
17796        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17797            return Err(self.err(format!(
17798                "expected KEY after ON DUPLICATE, got {:?}",
17799                self.peek()
17800            )));
17801        }
17802        self.advance();
17803        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17804            return Err(self.err(format!(
17805                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17806                self.peek()
17807            )));
17808        }
17809        self.advance();
17810        let mut assignments: Vec<(String, Expr)> = Vec::new();
17811        loop {
17812            let col = self.expect_ident_like()?;
17813            if !matches!(self.peek(), Token::Eq) {
17814                return Err(self.err(format!(
17815                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17816                    self.peek()
17817                )));
17818            }
17819            self.advance();
17820            let mut expr = self.parse_expr(0)?;
17821            Self::rewrite_mysql_values_refs(&mut expr);
17822            assignments.push((col, expr));
17823            if matches!(self.peek(), Token::Comma) {
17824                self.advance();
17825                continue;
17826            }
17827            break;
17828        }
17829        Ok(Some(crate::ast::OnConflictClause {
17830            target_columns: Vec::new(),
17831            index_where: None,
17832            constraint_name: None,
17833            mysql_lowered: true,
17834            action: crate::ast::OnConflictAction::Update {
17835                assignments,
17836                where_: None,
17837            },
17838        }))
17839    }
17840
17841    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17842        crate::ast::OnConflictClause {
17843            target_columns: Vec::new(),
17844            index_where: None,
17845            constraint_name: None,
17846            mysql_lowered: true,
17847            action: crate::ast::OnConflictAction::Nothing,
17848        }
17849    }
17850
17851    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17852        debug_assert!(
17853            matches!(self.peek(), Token::Insert)
17854                || (replace
17855                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17856        );
17857        self.advance();
17858        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17859        // would raise a duplicate-key error instead of failing the statement,
17860        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17861        // plain ident to the lexer; only the MySQL dialect accepts it here.
17862        let ignore = self.mysql_dialect
17863            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17864        if ignore {
17865            self.advance();
17866        }
17867        if !matches!(self.peek(), Token::Into) {
17868            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17869        }
17870        self.advance();
17871        let table = self.expect_ident_like()?;
17872        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17873        // grammar requires the AS keyword here (a bare identifier would be
17874        // ambiguous with a column list). The alias is what the ON CONFLICT
17875        // DO UPDATE expressions refer to the target row by.
17876        let alias = if matches!(self.peek(), Token::As) {
17877            self.advance();
17878            Some(self.expect_ident_like()?)
17879        } else {
17880            None
17881        };
17882        // v7.39 (round 428) — MySQL's SET-form INSERT:
17883        //     INSERT INTO t SET a = 1, b = 'x'
17884        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17885        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17886        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17887        // measured). So it lowers to the column list + one VALUES row and
17888        // rejoins the ordinary path, which already handles every one of
17889        // those. PG has no such spelling, hence the dialect gate.
17890        if self.mysql_dialect
17891            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17892        {
17893            self.advance(); // SET
17894            let mut names = Vec::new();
17895            let mut values = Vec::new();
17896            loop {
17897                names.push(self.expect_ident_like()?);
17898                if !matches!(self.peek(), Token::Eq) {
17899                    return Err(self.err(alloc::format!(
17900                        "expected '=' in INSERT … SET, got {:?}",
17901                        self.peek()
17902                    )));
17903                }
17904                self.advance();
17905                // `SET a = DEFAULT` rides the same `__column_default` marker
17906                // the VALUES-row and UPDATE-SET paths use; the INSERT
17907                // executor resolves it against the target column.
17908                if matches!(self.peek(), Token::Default) {
17909                    self.advance();
17910                    values.push(Expr::FunctionCall {
17911                        name: "__column_default".to_string(),
17912                        args: Vec::new(),
17913                    });
17914                } else {
17915                    values.push(self.parse_expr(0)?);
17916                }
17917                if matches!(self.peek(), Token::Comma) {
17918                    self.advance();
17919                    continue;
17920                }
17921                break;
17922            }
17923            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17924            let returning = self.parse_optional_returning()?;
17925            return Ok(Statement::Insert(InsertStatement {
17926                ctes: Vec::new(),
17927                table,
17928                alias,
17929                columns: Some(names),
17930                rows: alloc::vec![values],
17931                select_source: None,
17932                // MySQL's SET form has no `OVERRIDING …` clause (that is
17933                // PG's identity-column spelling).
17934                overriding: Overriding::None,
17935                mysql_ignore: ignore,
17936                on_conflict,
17937                returning,
17938            }));
17939        }
17940        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17941        // v7.39 (round 151) — a SELECT or WITH right after the paren is
17942        // a parenthesized query source instead (PG select_with_parens:
17943        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17944        // both keywords are reserved in PG, so no column list can start
17945        // with them.
17946        let columns = if matches!(self.peek(), Token::LParen) {
17947            self.advance();
17948            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17949                let select_stmt = if self.peek_is_with_kw() {
17950                    self.advance();
17951                    self.parse_nested_with_select()?
17952                } else {
17953                    match self.parse_select_stmt()? {
17954                        Statement::Select(s) => s,
17955                        other => {
17956                            return Err(self.err(alloc::format!(
17957                                "expected SELECT in parenthesized INSERT source, got {other:?}"
17958                            )));
17959                        }
17960                    }
17961                };
17962                if !matches!(self.peek(), Token::RParen) {
17963                    return Err(self.err(format!(
17964                        "expected ')' after parenthesized INSERT source, got {:?}",
17965                        self.peek()
17966                    )));
17967                }
17968                self.advance();
17969                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17970                let returning = self.parse_optional_returning()?;
17971                return Ok(Statement::Insert(InsertStatement {
17972                    ctes: Vec::new(),
17973                    table,
17974                    alias: alias.clone(),
17975                    columns: None,
17976                    rows: Vec::new(),
17977                    select_source: Some(Box::new(select_stmt)),
17978                    on_conflict,
17979                    returning,
17980                    overriding: Overriding::None,
17981                    mysql_ignore: ignore,
17982                }));
17983            }
17984            let mut names = Vec::new();
17985            loop {
17986                names.push(self.expect_ident_like()?);
17987                match self.peek() {
17988                    Token::Comma => {
17989                        self.advance();
17990                    }
17991                    Token::RParen => {
17992                        self.advance();
17993                        break;
17994                    }
17995                    other => {
17996                        return Err(self.err(format!(
17997                            "expected ',' or ')' in INSERT column list, got {other:?}"
17998                        )));
17999                    }
18000                }
18001            }
18002            Some(names)
18003        } else {
18004            None
18005        };
18006        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18007        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18008        // is captured on the statement so the engine can apply PG's
18009        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18010        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18011        {
18012            self.advance();
18013            let which = self.expect_ident_like()?;
18014            let ov = if which.eq_ignore_ascii_case("system") {
18015                Overriding::System
18016            } else if which.eq_ignore_ascii_case("user") {
18017                Overriding::User
18018            } else {
18019                return Err(self.err(format!(
18020                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18021                )));
18022            };
18023            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18024                return Err(self.err(format!(
18025                    "expected VALUE after OVERRIDING {}, got {:?}",
18026                    which.to_ascii_uppercase(),
18027                    self.peek()
18028                )));
18029            }
18030            self.advance();
18031            ov
18032        } else {
18033            Overriding::None
18034        };
18035        // `INSERT INTO t DEFAULT VALUES` — a single row made
18036        // entirely of column defaults. Lower to the permuted
18037        // column-list path with an empty list: every schema column
18038        // is unmapped, so the engine fills each from its default
18039        // (serials advance, plain defaults evaluate, the rest NULL).
18040        if matches!(self.peek(), Token::Default) {
18041            self.advance();
18042            if !matches!(self.peek(), Token::Values) {
18043                return Err(self.err(format!(
18044                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18045                    self.peek()
18046                )));
18047            }
18048            self.advance();
18049            if columns.is_some() {
18050                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18051            }
18052            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18053            let returning = self.parse_optional_returning()?;
18054            return Ok(Statement::Insert(InsertStatement {
18055                ctes: Vec::new(),
18056                table,
18057                alias: alias.clone(),
18058                columns: Some(Vec::new()),
18059                rows: alloc::vec![Vec::new()],
18060                select_source: None,
18061                on_conflict,
18062                returning,
18063                overriding,
18064                mysql_ignore: ignore,
18065            }));
18066        }
18067        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18068        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18069        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18070        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18071        // own WITH comes before INSERT).
18072        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18073            let select_stmt = if self.peek_is_with_kw() {
18074                self.advance();
18075                self.parse_nested_with_select()?
18076            } else {
18077                match self.parse_select_stmt()? {
18078                    Statement::Select(s) => s,
18079                    other => {
18080                        return Err(self.err(alloc::format!(
18081                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18082                        )));
18083                    }
18084                }
18085            };
18086            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18087            let returning = self.parse_optional_returning()?;
18088            return Ok(Statement::Insert(InsertStatement {
18089                ctes: Vec::new(),
18090                table,
18091                alias: alias.clone(),
18092                columns,
18093                rows: Vec::new(),
18094                select_source: Some(Box::new(select_stmt)),
18095                on_conflict,
18096                returning,
18097                overriding,
18098                mysql_ignore: ignore,
18099            }));
18100        }
18101        if !matches!(self.peek(), Token::Values) {
18102            return Err(self.err(format!(
18103                "expected VALUES or SELECT after table name, got {:?}",
18104                self.peek()
18105            )));
18106        }
18107        self.advance();
18108        if !matches!(self.peek(), Token::LParen) {
18109            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18110        }
18111        let mut rows = Vec::new();
18112        loop {
18113            // Each iteration consumes one `(expr, expr, …)` tuple.
18114            if !matches!(self.peek(), Token::LParen) {
18115                return Err(self.err(format!(
18116                    "expected '(' for next VALUES tuple, got {:?}",
18117                    self.peek()
18118                )));
18119            }
18120            self.advance();
18121            let mut tuple = Vec::new();
18122            loop {
18123                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18124                // the column's declared default for that slot. Rides out as the
18125                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18126                // path uses; the INSERT executor resolves it per target column.
18127                if matches!(self.peek(), Token::Default) {
18128                    self.advance();
18129                    tuple.push(Expr::FunctionCall {
18130                        name: "__column_default".to_string(),
18131                        args: Vec::new(),
18132                    });
18133                } else {
18134                    tuple.push(self.parse_expr(0)?);
18135                }
18136                match self.peek() {
18137                    Token::Comma => {
18138                        self.advance();
18139                    }
18140                    Token::RParen => {
18141                        self.advance();
18142                        break;
18143                    }
18144                    other => {
18145                        return Err(self.err(format!(
18146                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18147                        )));
18148                    }
18149                }
18150            }
18151            if tuple.is_empty() {
18152                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18153            }
18154            rows.push(tuple);
18155            // Continue with comma-separated tuples.
18156            if matches!(self.peek(), Token::Comma) {
18157                self.advance();
18158            } else {
18159                break;
18160            }
18161        }
18162        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18163        // to ON CONFLICT DO UPDATE with an empty conflict target
18164        // (the engine picks the table's first unique index, which
18165        // matches MySQL's any-unique-key behaviour for the common
18166        // single-key case). `VALUES(col)` in the assignments is
18167        // MySQL's spelling of EXCLUDED.col.
18168        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18169        let returning = self.parse_optional_returning()?;
18170        Ok(Statement::Insert(InsertStatement {
18171            ctes: Vec::new(),
18172            table,
18173            alias,
18174            columns,
18175            rows,
18176            select_source: None,
18177            on_conflict,
18178            returning,
18179            overriding,
18180            mysql_ignore: ignore,
18181        }))
18182    }
18183
18184    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18185    /// the incoming row's value — exactly PG's EXCLUDED.col.
18186    fn rewrite_mysql_values_refs(e: &mut Expr) {
18187        match e {
18188            Expr::FunctionCall { name, args }
18189                if name.eq_ignore_ascii_case("values")
18190                    && args.len() == 1
18191                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18192            {
18193                let Expr::Column(c) = &args[0] else {
18194                    unreachable!("guarded above");
18195                };
18196                *e = Expr::Column(crate::ast::ColumnName {
18197                    qualifier: Some("EXCLUDED".to_string()),
18198                    name: c.name.clone(),
18199                });
18200            }
18201            Expr::FunctionCall { args, .. } => {
18202                for a in args {
18203                    Self::rewrite_mysql_values_refs(a);
18204                }
18205            }
18206            Expr::Binary { lhs, rhs, .. } => {
18207                Self::rewrite_mysql_values_refs(lhs);
18208                Self::rewrite_mysql_values_refs(rhs);
18209            }
18210            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18211                Self::rewrite_mysql_values_refs(expr);
18212            }
18213            Expr::Case {
18214                operand,
18215                branches,
18216                else_branch,
18217            } => {
18218                if let Some(op) = operand {
18219                    Self::rewrite_mysql_values_refs(op);
18220                }
18221                for (w, t) in branches {
18222                    Self::rewrite_mysql_values_refs(w);
18223                    Self::rewrite_mysql_values_refs(t);
18224                }
18225                if let Some(el) = else_branch {
18226                    Self::rewrite_mysql_values_refs(el);
18227                }
18228            }
18229            _ => {}
18230        }
18231    }
18232
18233    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18234    /// clause sitting between the INSERT body and the trailing
18235    /// RETURNING. All keywords come in as bare idents; `ON` is
18236    /// a reserved Token though.
18237    fn parse_optional_on_conflict(
18238        &mut self,
18239    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18240        if !matches!(self.peek(), Token::On) {
18241            return Ok(None);
18242        }
18243        // Peek further: we want exactly "ON CONFLICT ...". If the
18244        // next ident isn't "conflict", let some other parser handle.
18245        let next_is_conflict = matches!(
18246            self.tokens.get(self.pos + 1),
18247            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18248        );
18249        if !next_is_conflict {
18250            return Ok(None);
18251        }
18252        self.advance(); // ON
18253        self.advance(); // CONFLICT
18254        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18255        // the constraint instead of listing columns (the pg_dump
18256        // form); the engine resolves it.
18257        let mut constraint_name: Option<String> = None;
18258        if matches!(self.peek(), Token::On) {
18259            self.advance(); // ON
18260            match self.advance() {
18261                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18262                }
18263                other => {
18264                    return Err(self.err(alloc::format!(
18265                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18266                    )));
18267                }
18268            }
18269            constraint_name = Some(self.expect_ident_like()?);
18270        }
18271        // Optional `(col [, col]*)` target list.
18272        let mut target_columns: Vec<String> = Vec::new();
18273        if matches!(self.peek(), Token::LParen) {
18274            self.advance();
18275            loop {
18276                target_columns.push(self.expect_ident_like()?);
18277                match self.peek() {
18278                    Token::Comma => {
18279                        self.advance();
18280                    }
18281                    Token::RParen => {
18282                        self.advance();
18283                        break;
18284                    }
18285                    other => {
18286                        return Err(self.err(alloc::format!(
18287                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18288                        )));
18289                    }
18290                }
18291            }
18292        }
18293        // v7.39 (round 240) — optional index predicate after the target
18294        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18295        // PARTIAL unique index; SPG's arbiters are full indexes, which
18296        // satisfy any predicate, so it is parsed and carried but not
18297        // consulted (recorded residual: partial-unique-index arbiters).
18298        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18299            self.advance();
18300            Some(self.parse_expr(0)?)
18301        } else {
18302            None
18303        };
18304        // Required `DO`.
18305        match self.advance() {
18306            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18307            other => {
18308                return Err(self.err(alloc::format!(
18309                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18310                )));
18311            }
18312        }
18313        // Action: NOTHING | UPDATE SET …
18314        let action = match self.advance() {
18315            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18316                crate::ast::OnConflictAction::Nothing
18317            }
18318            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18319                self.parse_on_conflict_update_action()?
18320            }
18321            other => {
18322                return Err(self.err(alloc::format!(
18323                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18324                )));
18325            }
18326        };
18327        Ok(Some(crate::ast::OnConflictClause {
18328            target_columns,
18329            index_where,
18330            constraint_name,
18331            mysql_lowered: false,
18332            action,
18333        }))
18334    }
18335
18336    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18337    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18338    /// consumed `UPDATE`.
18339    fn parse_on_conflict_update_action(
18340        &mut self,
18341    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18342        // `SET`
18343        match self.advance() {
18344            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18345            other => {
18346                return Err(self.err(alloc::format!(
18347                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18348                )));
18349            }
18350        }
18351        let mut assignments: Vec<(String, Expr)> = Vec::new();
18352        loop {
18353            let col = self.expect_ident_like()?;
18354            if !matches!(self.peek(), Token::Eq) {
18355                return Err(self.err(alloc::format!(
18356                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18357                    self.peek()
18358                )));
18359            }
18360            self.advance();
18361            let value = self.parse_expr(0)?;
18362            assignments.push((col, value));
18363            if matches!(self.peek(), Token::Comma) {
18364                self.advance();
18365                continue;
18366            }
18367            break;
18368        }
18369        let where_ = if matches!(self.peek(), Token::Where) {
18370            self.advance();
18371            Some(self.parse_expr(0)?)
18372        } else {
18373            None
18374        };
18375        Ok(crate::ast::OnConflictAction::Update {
18376            assignments,
18377            where_,
18378        })
18379    }
18380
18381    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18382        let mut items = Vec::new();
18383        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18384        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18385        // answers one zero-column row per row of t, and a bare `SELECT`
18386        // answers a single zero-column row. SPG required at least one
18387        // item, so both were syntax errors. Recognised by the token that
18388        // follows — nothing that can start an expression appears here.
18389        if self.select_list_is_empty_here() {
18390            return Ok(items);
18391        }
18392        loop {
18393            items.push(self.parse_select_item()?);
18394            if matches!(self.peek(), Token::Comma) {
18395                self.advance();
18396            } else {
18397                break;
18398            }
18399        }
18400        Ok(items)
18401    }
18402
18403    /// Is the target list empty at this point — i.e. does the next token
18404    /// end the SELECT's item list rather than start an item?
18405    fn select_list_is_empty_here(&self) -> bool {
18406        match self.peek() {
18407            Token::From
18408            | Token::Where
18409            | Token::Group
18410            | Token::Having
18411            | Token::Order
18412            | Token::Limit
18413            | Token::Offset
18414            | Token::Semicolon
18415            | Token::RParen
18416            | Token::Union
18417            | Token::Except
18418            | Token::Eof => true,
18419            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18420            // with unreserved keywords, so they arrive as plain idents.
18421            Token::Ident(s) => {
18422                s.eq_ignore_ascii_case("fetch")
18423                    || s.eq_ignore_ascii_case("window")
18424                    || s.eq_ignore_ascii_case("intersect")
18425            }
18426            _ => false,
18427        }
18428    }
18429
18430    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18431        if matches!(self.peek(), Token::Star) {
18432            self.advance();
18433            return Ok(SelectItem::Wildcard);
18434        }
18435        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18436        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18437        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18438        // `<ident> . *` with nothing binding tighter.
18439        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18440            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18441                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18442            {
18443                self.advance(); // qualifier
18444                self.advance(); // .
18445                self.advance(); // *
18446                return Ok(SelectItem::QualifiedWildcard(q));
18447            }
18448        }
18449        let start_tok = self.pos;
18450        let expr = self.parse_expr(0)?;
18451        let end_tok = self.consumed_pos();
18452        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18453        // multi-column function returns into columns. Marked here and lowered in
18454        // `parse_bare_select`, where the FROM clause is in hand.
18455        if matches!(self.peek(), Token::Dot)
18456            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18457        {
18458            self.advance(); // .
18459            self.advance(); // *
18460            return Ok(SelectItem::Expr {
18461                expr: Expr::FunctionCall {
18462                    name: "__record_expand".to_string(),
18463                    args: alloc::vec![expr],
18464                },
18465                alias: None,
18466            });
18467        }
18468        let alias = match self.parse_optional_alias()? {
18469            Some(a) => Some(a),
18470            None => self.mysql_item_label(&expr, start_tok, end_tok),
18471        };
18472        Ok(SelectItem::Expr { expr, alias })
18473    }
18474
18475    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18476    /// carries no `AS`, filled in here so every downstream path reports it
18477    /// without knowing the rule. `None` leaves the item un-aliased, which is
18478    /// what a PG session always gets.
18479    ///
18480    /// Measured against MariaDB 11, three rules and no more:
18481    ///
18482    /// | item             | label      | why                          |
18483    /// |------------------|------------|------------------------------|
18484    /// | `lbl.a`          | `a`        | a column reports its name    |
18485    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18486    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18487    ///
18488    /// The third is why this lives in the parser at all: the label is the
18489    /// text the client WROTE, down to the spacing, so it cannot be printed
18490    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18491    ///
18492    /// Comments survive, and that is right: through a `mariadb` CLI both
18493    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18494    /// CLIENT stripping the comment before it sends. Asked over the raw
18495    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18496    /// produces.
18497    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18498        if !self.mysql_dialect {
18499            return None;
18500        }
18501        match expr {
18502            // A column already reports its own name downstream; naming it
18503            // again here would only re-state the qualifier the label drops.
18504            Expr::Column(_) => None,
18505            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18506            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18507        }
18508    }
18509
18510    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18511    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18512    /// with PG's default column1..columnN names; subsequent rows
18513    /// chain as UNION ALL peers. Shared by the FROM-position
18514    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18515    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18516        let mut row_selects: Vec<SelectStatement> = Vec::new();
18517        loop {
18518            if !matches!(self.peek(), Token::LParen) {
18519                return Err(self.err(alloc::format!(
18520                    "expected '(' to start a VALUES row, got {:?}",
18521                    self.peek()
18522                )));
18523            }
18524            self.advance(); // (
18525            let mut items: Vec<SelectItem> = Vec::new();
18526            loop {
18527                let expr = self.parse_expr(0)?;
18528                items.push(SelectItem::Expr {
18529                    expr,
18530                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18531                });
18532                match self.peek() {
18533                    Token::Comma => {
18534                        self.advance();
18535                    }
18536                    Token::RParen => break,
18537                    other => {
18538                        return Err(self.err(alloc::format!(
18539                            "expected ',' or ')' in VALUES row, got {other:?}"
18540                        )));
18541                    }
18542                }
18543            }
18544            self.advance(); // )
18545            row_selects.push(SelectStatement {
18546                locking: None,
18547                ctes: Vec::new(),
18548                distinct: false,
18549                distinct_on: Vec::new(),
18550                items,
18551                from: None,
18552                where_: None,
18553                group_by: None,
18554                group_by_all: false,
18555                having: None,
18556                unions: Vec::new(),
18557                order_by: Vec::new(),
18558                limit: None,
18559                offset: None,
18560                limit_with_ties: false,
18561                window_check_exprs: Vec::new(),
18562            });
18563            if matches!(self.peek(), Token::Comma) {
18564                self.advance();
18565                continue;
18566            }
18567            break;
18568        }
18569        let mut head = row_selects.remove(0);
18570        head.unions = row_selects
18571            .into_iter()
18572            .map(|s| (UnionKind::All, s))
18573            .collect();
18574        Ok(head)
18575    }
18576
18577    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18578        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18579        // children. It was read as a table NAMED `only`, so the query
18580        // failed on `relation "only" does not exist`.
18581        //
18582        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18583        // absorbed the keyword, reasoning that SPG's children are
18584        // separate relations a plain scan does not descend into, so ONLY
18585        // already described the scan. That stopped being true when a
18586        // partition parent started unioning its children: measured,
18587        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18588        // where PG answers 0. The flag is carried now.
18589        let mut only = false;
18590        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18591            && matches!(
18592                self.tokens.get(self.pos + 1),
18593                Some(Token::Ident(_) | Token::QuotedIdent(_))
18594            )
18595        {
18596            only = true;
18597            self.advance();
18598        }
18599        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18600        // for these SRFs the keyword is noise at parse time: the
18601        // join executor already substitutes outer-column references
18602        // into unnest_expr / generate_series_args per outer row
18603        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18604        // licences the correlation even without the keyword. Absorb
18605        // it and fall through to the SRF arms below.
18606        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18607        // just the four builtin SRFs: a user set-returning function on a JOIN's
18608        // right side is the whole point of LATERAL. The keyword stays noise at
18609        // parse time — the join executor substitutes the outer row into the
18610        // call's arguments per outer row.
18611        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18612            && matches!(
18613                self.tokens.get(self.pos + 1),
18614                // The json_each family has its OWN `LATERAL …` arm below, which
18615                // needs to see the keyword — absorbing it here would send those
18616                // calls down the generic table-function channel instead.
18617                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18618            )
18619            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18620        {
18621            self.advance(); // LATERAL
18622        }
18623        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18624        // set-returning function whose argument may reference a
18625        // preceding FROM item. We rewrite this to
18626        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18627        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18628        // executor handles per-outer-row evaluation and the
18629        // SRF-primary jsonb_each_text path handles the inner
18630        // materialisation. Sentori 0067 backfill is the dogfood
18631        // shape.
18632        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18633            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18634            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18635        {
18636            self.advance(); // LATERAL
18637            let each_fn = match self.peek() {
18638                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18639                _ => unreachable!(),
18640            };
18641            self.advance(); // jsonb_each[_text] / json_each[_text]
18642            self.advance(); // (
18643            let arg = self.parse_expr(0)?;
18644            if !matches!(self.peek(), Token::RParen) {
18645                return Err(self.err(alloc::format!(
18646                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18647                    self.peek()
18648                )));
18649            }
18650            self.advance();
18651            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18652            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18653            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18654            //               FROM jsonb_each_text(<arg>) AS __srf__
18655            // PG's `AS kv(key, value)` column-alias list maps
18656            // positions to names; default to (key, value) when
18657            // omitted (matching the SRF's natural column names).
18658            let srf_alias = "__srf__".to_string();
18659            let key_alias = column_aliases
18660                .first()
18661                .cloned()
18662                .unwrap_or_else(|| "key".to_string());
18663            let value_alias = column_aliases
18664                .get(1)
18665                .cloned()
18666                .unwrap_or_else(|| "value".to_string());
18667            let inner_select = crate::ast::SelectStatement {
18668                locking: None,
18669                ctes: Vec::new(),
18670                distinct: false,
18671                distinct_on: Vec::new(),
18672                items: alloc::vec![
18673                    crate::ast::SelectItem::Expr {
18674                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18675                            qualifier: Some(srf_alias.clone()),
18676                            name: "key".to_string(),
18677                        }),
18678                        alias: Some(key_alias),
18679                    },
18680                    crate::ast::SelectItem::Expr {
18681                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18682                            qualifier: Some(srf_alias.clone()),
18683                            name: "value".to_string(),
18684                        }),
18685                        alias: Some(value_alias),
18686                    },
18687                ],
18688                from: Some(crate::ast::FromClause {
18689                    primary: TableRef {
18690                        name: srf_alias.clone(),
18691                        alias: Some(srf_alias.clone()),
18692                        only: false,
18693                        as_of_segment: None,
18694                        unnest_expr: None,
18695                        unnest_column_aliases: Vec::new(),
18696                        with_ordinality: false,
18697                        generate_series_args: None,
18698                        lateral_subquery: None,
18699                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18700                        table_fn_call: None,
18701                        rows_from: None,
18702                        json_table: None,
18703                        scalar_fn_item: false,
18704                    },
18705                    joins: Vec::new(),
18706                }),
18707                where_: None,
18708                group_by: None,
18709                group_by_all: false,
18710                having: None,
18711                unions: Vec::new(),
18712                order_by: Vec::new(),
18713                limit: None,
18714                offset: None,
18715                limit_with_ties: false,
18716                window_check_exprs: Vec::new(),
18717            };
18718            return Ok(TableRef {
18719                name: alias.clone(),
18720                alias: Some(alias),
18721                only: false,
18722                as_of_segment: None,
18723                unnest_expr: None,
18724                unnest_column_aliases: Vec::new(),
18725                with_ordinality: false,
18726                generate_series_args: None,
18727                lateral_subquery: Some(Box::new(inner_select)),
18728                jsonb_each_text_arg: None,
18729                table_fn_call: None,
18730                rows_from: None,
18731                json_table: None,
18732                scalar_fn_item: false,
18733            });
18734        }
18735        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18736        // without an explicit `LATERAL` keyword is the same shape
18737        // PG accepts (SRF naturally licences lateral correlation).
18738        // We mirror the LATERAL rewrite when the argument syntactic-
18739        // ally references an outer column (Column { qualifier:
18740        // Some(_), … }). For simplicity we apply the rewrite
18741        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18742        // in the FROM-list — caller-side join parsing positions
18743        // this peek correctly.
18744        // (Implementation note: detection lives below; the LATERAL
18745        // branch above already covers the explicit form; the bare
18746        // form falls through to the plain SRF arm and the engine
18747        // treats it as a constant-arg SRF if no outer reference is
18748        // present.)
18749        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18750        // table. Detect at the head so it claims precedence over
18751        // every other table-ref shape (unnest / generate_series /
18752        // bare ident); the lateral subquery itself follows the
18753        // regular SELECT grammar.
18754        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18755        // t(cols)`. Each row lowers to a constant SELECT with PG's
18756        // default column1..columnN names; subsequent rows chain as
18757        // UNION ALL peers. The result rides the derived-table
18758        // lateral_subquery channel — zero executor work.
18759        if matches!(self.peek(), Token::LParen)
18760            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18761        {
18762            self.advance(); // (
18763            self.advance(); // VALUES
18764            let head = self.parse_values_rows_body()?;
18765            if !matches!(self.peek(), Token::RParen) {
18766                return Err(self.err(alloc::format!(
18767                    "expected ')' after VALUES list, got {:?}",
18768                    self.peek()
18769                )));
18770            }
18771            self.advance();
18772            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18773            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18774            return Ok(TableRef {
18775                name,
18776                alias: alias_ident,
18777                only: false,
18778                as_of_segment: None,
18779                unnest_expr: None,
18780                unnest_column_aliases: column_aliases,
18781                with_ordinality: false,
18782                generate_series_args: None,
18783                lateral_subquery: Some(Box::new(head)),
18784                jsonb_each_text_arg: None,
18785                table_fn_call: None,
18786                rows_from: None,
18787                json_table: None,
18788                scalar_fn_item: false,
18789            });
18790        }
18791        // v7.37.17 (17.6 siblings) — plain derived table:
18792        // `FROM ( SELECT … ) [AS] alias`. Rides the same
18793        // lateral_subquery channel the explicit LATERAL form uses —
18794        // an uncorrelated inner SELECT executes identically. The
18795        // inner parse carries UNION tails (they live on
18796        // SelectStatement.unions).
18797        // v7.37 D.20 — the derived-table inner may itself be a
18798        // parenthesized set-operation group (`FROM ((SELECT…) UNION
18799        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18800        // bare `(SELECT …)`. parse_one_statement already routes a leading
18801        // `(` set-op group (its LParen arm) and a leading WITH
18802        // (parse_with_cte_then_select), so widen the second-token gate to
18803        // Select | LParen | WITH.
18804        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18805        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18806        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18807        // has existed since the shorthand landed and `parse_bare_select`
18808        // already routes it ("valid anywhere a SELECT head is"); what was
18809        // missing is this second-token gate, and the CTE body's dispatch
18810        // below. Round 868 found both by putting the shorthand in a
18811        // subquery — the top-level forms had been the only ones tested.
18812        if matches!(self.peek(), Token::LParen)
18813            && (matches!(
18814                self.tokens.get(self.pos + 1),
18815                Some(Token::Select | Token::LParen | Token::Table)
18816            ) || matches!(self.tokens.get(self.pos + 1),
18817                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18818        {
18819            self.advance(); // (
18820            let inner = match self.parse_one_statement()? {
18821                Statement::Select(s) => s,
18822                other => {
18823                    return Err(self.err(alloc::format!(
18824                        "expected SELECT inside derived table ( … ), got {other:?}"
18825                    )));
18826                }
18827            };
18828            if !matches!(self.peek(), Token::RParen) {
18829                return Err(self.err(alloc::format!(
18830                    "expected ')' after derived-table subquery, got {:?}",
18831                    self.peek()
18832                )));
18833            }
18834            self.advance();
18835            // `AS t(a, b)` column-alias list rides the
18836            // unnest_column_aliases field (same positional-rename
18837            // contract the unnest SRFs use).
18838            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18839            let name = alias_ident
18840                .clone()
18841                .unwrap_or_else(|| "subquery".to_string());
18842            return Ok(TableRef {
18843                name,
18844                alias: alias_ident,
18845                only: false,
18846                as_of_segment: None,
18847                unnest_expr: None,
18848                unnest_column_aliases: column_aliases,
18849                with_ordinality: false,
18850                generate_series_args: None,
18851                lateral_subquery: Some(Box::new(inner)),
18852                jsonb_each_text_arg: None,
18853                table_fn_call: None,
18854                rows_from: None,
18855                json_table: None,
18856                scalar_fn_item: false,
18857            });
18858        }
18859        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18860            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18861        {
18862            self.advance(); // LATERAL
18863            self.advance(); // (
18864            // Parse the inner SELECT.
18865            let inner = match self.parse_one_statement()? {
18866                Statement::Select(s) => s,
18867                other => {
18868                    return Err(self.err(alloc::format!(
18869                        "expected SELECT inside LATERAL ( … ), got {other:?}"
18870                    )));
18871                }
18872            };
18873            if !matches!(self.peek(), Token::RParen) {
18874                return Err(self.err(alloc::format!(
18875                    "expected ')' after LATERAL subquery, got {:?}",
18876                    self.peek()
18877                )));
18878            }
18879            self.advance();
18880            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18881            // `(VALUES …) t(g)` derived table round-trips through view-body
18882            // Display, which renders on the lateral_subquery channel).
18883            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18884            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18885            return Ok(TableRef {
18886                name,
18887                alias: alias_ident,
18888                only: false,
18889                as_of_segment: None,
18890                unnest_expr: None,
18891                unnest_column_aliases: column_aliases,
18892                with_ordinality: false,
18893                generate_series_args: None,
18894                lateral_subquery: Some(Box::new(inner)),
18895                jsonb_each_text_arg: None,
18896                table_fn_call: None,
18897                rows_from: None,
18898                json_table: None,
18899                scalar_fn_item: false,
18900            });
18901        }
18902        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18903        // function as a FROM item. Emits one row per (key, value)
18904        // pair in the JSONB object argument as TEXT columns. May
18905        // be wrapped in CROSS JOIN LATERAL when the argument
18906        // references a preceding FROM item (sentori migration
18907        // 0067 backfill shape: `CROSS JOIN LATERAL
18908        // jsonb_each_text(t.json_col) AS kv(key, value)`).
18909        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18910            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18911        {
18912            let each_fn = match self.peek() {
18913                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18914                _ => unreachable!(),
18915            };
18916            self.advance(); // jsonb_each[_text] / json_each[_text]
18917            self.advance(); // (
18918            let arg = self.parse_expr(0)?;
18919            if !matches!(self.peek(), Token::RParen) {
18920                return Err(self.err(alloc::format!(
18921                    "expected ')' after {each_fn}() argument, got {:?}",
18922                    self.peek()
18923                )));
18924            }
18925            self.advance();
18926            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18927            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18928            return Ok(TableRef {
18929                name,
18930                alias: alias_ident,
18931                only: false,
18932                as_of_segment: None,
18933                unnest_expr: None,
18934                // `AS t(k, v)` renames key/value positionally, same as the
18935                // LATERAL-position form already does.
18936                unnest_column_aliases: column_aliases,
18937                with_ordinality: false,
18938                generate_series_args: None,
18939                lateral_subquery: None,
18940                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18941                table_fn_call: None,
18942                rows_from: None,
18943                json_table: None,
18944                scalar_fn_item: false,
18945            });
18946        }
18947        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18948        // (+ json_ variants) — record-returning JSON functions with a
18949        // column-definition list. Desugar to a derived table that
18950        // projects each declared column from the JSON via `->>` + a cast,
18951        // over `jsonb_array_elements(J)` for the *set (per-element) form.
18952        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18953            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18954        {
18955            return self.parse_json_to_record_from();
18956        }
18957        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18958        // row is a text[] of capture groups, so it cannot desugar to unnest
18959        // (that would flatten the array). Wrap it as a derived table
18960        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18961        // SRF path already emits one text[] row per match. PG names the column
18962        // `regexp_matches`; an `AS a(col)` alias overrides it.
18963        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18964                if s.eq_ignore_ascii_case("regexp_matches"))
18965            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18966        {
18967            self.advance(); // fn name
18968            self.advance(); // (
18969            let mut fn_args: Vec<Expr> = Vec::new();
18970            loop {
18971                fn_args.push(self.parse_expr(0)?);
18972                if matches!(self.peek(), Token::Comma) {
18973                    self.advance();
18974                    continue;
18975                }
18976                break;
18977            }
18978            if !matches!(self.peek(), Token::RParen) {
18979                return Err(self.err(alloc::format!(
18980                    "expected ')' after regexp_matches() arguments, got {:?}",
18981                    self.peek()
18982                )));
18983            }
18984            self.advance();
18985            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18986            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18987            // it, so it died on the `with` token while every other table function
18988            // accepted it.
18989            let with_ordinality = self.absorb_with_ordinality();
18990            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18991            let table_alias = alias_ident
18992                .clone()
18993                .unwrap_or_else(|| "regexp_matches".to_string());
18994            // PG names a single-column function's output column after the ALIAS
18995            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18996            // `m` reads as that column and not as a whole-row composite. Naming
18997            // it after the function regardless made `SELECT m[1] FROM … AS m`
18998            // subscript a record.
18999            let col_name = column_aliases
19000                .first()
19001                .cloned()
19002                .or_else(|| alias_ident.clone())
19003                .unwrap_or_else(|| "regexp_matches".to_string());
19004            let inner = crate::ast::SelectStatement {
19005                locking: None,
19006                ctes: Vec::new(),
19007                distinct: false,
19008                distinct_on: Vec::new(),
19009                items: alloc::vec![SelectItem::Expr {
19010                    expr: Expr::FunctionCall {
19011                        name: "regexp_matches".to_string(),
19012                        args: fn_args,
19013                    },
19014                    alias: Some(col_name),
19015                }],
19016                from: None,
19017                where_: None,
19018                group_by: None,
19019                group_by_all: false,
19020                having: None,
19021                unions: Vec::new(),
19022                order_by: Vec::new(),
19023                limit: None,
19024                offset: None,
19025                limit_with_ties: false,
19026                window_check_exprs: Vec::new(),
19027            };
19028            return Ok(TableRef {
19029                name: table_alias.clone(),
19030                alias: Some(table_alias),
19031                only: false,
19032                as_of_segment: None,
19033                unnest_expr: None,
19034                unnest_column_aliases: column_aliases,
19035                with_ordinality,
19036                generate_series_args: None,
19037                lateral_subquery: Some(Box::new(inner)),
19038                jsonb_each_text_arg: None,
19039                table_fn_call: None,
19040                rows_from: None,
19041                json_table: None,
19042                // regexp_matches returns text[], a base type: `SELECT m FROM
19043                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19044                scalar_fn_item: true,
19045            });
19046        }
19047        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19048        // / json_ variants as a FROM item. Rewritten into
19049        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19050        // elements as a TEXT array, and the existing unnest SRF path
19051        // materialises one row per element. PG's natural column name
19052        // is `value`; an `AS a(col)` column-alias list overrides it.
19053        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19054                if s.eq_ignore_ascii_case("jsonb_array_elements")
19055                    || s.eq_ignore_ascii_case("json_array_elements")
19056                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19057                    || s.eq_ignore_ascii_case("json_array_elements_text")
19058                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19059                    || s.eq_ignore_ascii_case("json_object_keys")
19060                    || s.eq_ignore_ascii_case("jsonb_path_query")
19061                    || s.eq_ignore_ascii_case("json_path_query")
19062                    || s.eq_ignore_ascii_case("generate_subscripts")
19063                    || s.eq_ignore_ascii_case("string_to_table")
19064                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19065            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19066        {
19067            let fn_name = match self.peek() {
19068                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19069                _ => unreachable!(),
19070            };
19071            self.advance(); // fn name
19072            self.advance(); // (
19073            let mut fn_args: Vec<Expr> = Vec::new();
19074            loop {
19075                fn_args.push(self.parse_expr(0)?);
19076                if matches!(self.peek(), Token::Comma) {
19077                    self.advance();
19078                    continue;
19079                }
19080                break;
19081            }
19082            if !matches!(self.peek(), Token::RParen) {
19083                return Err(self.err(alloc::format!(
19084                    "expected ')' after {fn_name}() arguments, got {:?}",
19085                    self.peek()
19086                )));
19087            }
19088            self.advance();
19089            let with_ordinality = self.absorb_with_ordinality();
19090            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19091            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19092            // PG's natural column name: the array-elements SRFs
19093            // declare an OUT parameter `value`; jsonb_object_keys
19094            // and generate_subscripts have none, so the column is
19095            // named after the function. A bare table alias on a
19096            // single-column SRF renames the column too (PG: `FROM
19097            // generate_subscripts(a, 1) AS s` projects column s) —
19098            // except for the OUT-parameter SRFs, whose column stays
19099            // `value` under a bare alias.
19100            let natural_col = if fn_name.ends_with("_array_elements")
19101                || fn_name.ends_with("_array_elements_text")
19102            {
19103                "value".to_string()
19104            } else {
19105                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19106            };
19107            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19108            // Keep any further entries — the second names the
19109            // ordinality column under WITH ORDINALITY.
19110            srf_cols.extend(column_aliases.into_iter().skip(1));
19111            // The *_to_table SRFs are row-streams over the existing
19112            // *_to_array scalars — map the call target; the display
19113            // name (alias / column defaults) keeps the SRF spelling.
19114            let call_name = match fn_name.as_str() {
19115                "string_to_table" => "string_to_array".to_string(),
19116                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19117                _ => fn_name,
19118            };
19119            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19120            // preceding FROM item (bare or qualified column) is correlated;
19121            // route it through the per-outer-row lateral channel.
19122            let expr = crate::ast::Expr::FunctionCall {
19123                name: call_name,
19124                args: fn_args,
19125            };
19126            let correlated = Self::expr_has_any_column(&expr);
19127            let tref = TableRef {
19128                name,
19129                alias: alias_ident,
19130                only: false,
19131                as_of_segment: None,
19132                unnest_expr: Some(Box::new(expr)),
19133                unnest_column_aliases: srf_cols,
19134                with_ordinality,
19135                generate_series_args: None,
19136                lateral_subquery: None,
19137                jsonb_each_text_arg: None,
19138                table_fn_call: None,
19139                rows_from: None,
19140                json_table: None,
19141                // Each of these returns a BASE type (jsonb / text / int), so the item's
19142                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19143                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19144                scalar_fn_item: !with_ordinality,
19145            };
19146            return Ok(if correlated {
19147                Self::wrap_correlated_srf(tref)
19148            } else {
19149                tref
19150            });
19151        }
19152        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19153        // explicit parallel-zip syntax. Each entry lowers to its
19154        // array-returning scalar form (unnest(x) → x itself; the
19155        // FROM-SRF rewrite family → their scalar array calls) and
19156        // the list rides the multi-arg unnest zip channel:
19157        // NULL-padded to the longest, WITH ORDINALITY appends the
19158        // counter. generate_series has no scalar array form and
19159        // errors honestly.
19160        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19161            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19162            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19163        {
19164            self.advance(); // ROWS
19165            self.advance(); // FROM
19166            self.advance(); // (
19167            let mut entries: Vec<Expr> = Vec::new();
19168            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19169            // Used only when some entry has no array form.
19170            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19171            loop {
19172                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19173                if !matches!(self.peek(), Token::LParen) {
19174                    return Err(self.err(alloc::format!(
19175                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19176                        self.peek()
19177                    )));
19178                }
19179                self.advance();
19180                let mut fn_args: Vec<Expr> = Vec::new();
19181                if !matches!(self.peek(), Token::RParen) {
19182                    loop {
19183                        fn_args.push(self.parse_expr(0)?);
19184                        if matches!(self.peek(), Token::Comma) {
19185                            self.advance();
19186                            continue;
19187                        }
19188                        break;
19189                    }
19190                }
19191                if !matches!(self.peek(), Token::RParen) {
19192                    return Err(self.err(alloc::format!(
19193                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19194                        self.peek()
19195                    )));
19196                }
19197                self.advance();
19198                let entry = match fn_name.as_str() {
19199                    "unnest" => {
19200                        if fn_args.len() != 1 {
19201                            return Err(
19202                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19203                            );
19204                        }
19205                        fn_args.pop().expect("len checked")
19206                    }
19207                    "jsonb_array_elements"
19208                    | "json_array_elements"
19209                    | "jsonb_array_elements_text"
19210                    | "json_array_elements_text"
19211                    | "jsonb_object_keys"
19212                    | "json_object_keys"
19213                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19214                        name: fn_name,
19215                        args: fn_args,
19216                    },
19217                    "string_to_table" => crate::ast::Expr::FunctionCall {
19218                        name: "string_to_array".to_string(),
19219                        args: fn_args,
19220                    },
19221                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19222                        name: "regexp_split_to_array".to_string(),
19223                        args: fn_args,
19224                    },
19225                    // v7.39 (read01 round 74) — an SRF with no array form
19226                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19227                    // scalar expression to zip, so the WHOLE list switches to the
19228                    // rows_from channel, which runs each function and zips the
19229                    // rows themselves. The all-array case keeps the old lowering:
19230                    // it is well-trodden and this must not disturb it.
19231                    _ => {
19232                        generic.push((fn_name, fn_args));
19233                        if matches!(self.peek(), Token::Comma) {
19234                            self.advance();
19235                            continue;
19236                        }
19237                        break;
19238                    }
19239                };
19240                generic.push((
19241                    // The array-able entries carry their lowered expr along, so a
19242                    // MIXED list still works: the engine sees the scalar array
19243                    // form and unnests it.
19244                    "__array".to_string(),
19245                    alloc::vec![entry.clone()],
19246                ));
19247                entries.push(entry);
19248                if matches!(self.peek(), Token::Comma) {
19249                    self.advance();
19250                    continue;
19251                }
19252                break;
19253            }
19254            if !matches!(self.peek(), Token::RParen) {
19255                return Err(self.err(alloc::format!(
19256                    "expected ')' to close ROWS FROM, got {:?}",
19257                    self.peek()
19258                )));
19259            }
19260            self.advance();
19261            let with_ordinality = self.absorb_with_ordinality();
19262            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19263            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19264            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19265            // list rides the generic channel.
19266            if generic.iter().any(|(n, _)| n != "__array") {
19267                let correlated = generic
19268                    .iter()
19269                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19270                let tref = TableRef {
19271                    name,
19272                    alias: alias_ident,
19273                    only: false,
19274                    as_of_segment: None,
19275                    unnest_expr: None,
19276                    unnest_column_aliases,
19277                    with_ordinality,
19278                    generate_series_args: None,
19279                    lateral_subquery: None,
19280                    jsonb_each_text_arg: None,
19281                    table_fn_call: None,
19282                    rows_from: Some(generic),
19283                    json_table: None,
19284                    scalar_fn_item: false,
19285                };
19286                return Ok(if correlated {
19287                    Self::wrap_correlated_srf(tref)
19288                } else {
19289                    tref
19290                });
19291            }
19292            let correlated = entries.iter().any(Self::expr_has_any_column);
19293            let expr = if entries.len() == 1 {
19294                entries.pop().expect("len checked")
19295            } else {
19296                crate::ast::Expr::FunctionCall {
19297                    name: "__unnest_zip".to_string(),
19298                    args: entries,
19299                }
19300            };
19301            let tref = TableRef {
19302                name,
19303                alias: alias_ident,
19304                only: false,
19305                as_of_segment: None,
19306                unnest_expr: Some(Box::new(expr)),
19307                unnest_column_aliases,
19308                with_ordinality,
19309                generate_series_args: None,
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.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19324        // source. Detect at the head before the bare-ident fallback;
19325        // unnest is not a reserved token.
19326        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19327            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19328        {
19329            self.advance(); // unnest
19330            self.advance(); // (
19331            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19332            while matches!(self.peek(), Token::Comma) {
19333                self.advance();
19334                srf_args.push(self.parse_expr(0)?);
19335            }
19336            if !matches!(self.peek(), Token::RParen) {
19337                return Err(self.err(alloc::format!(
19338                    "expected ')' after unnest() argument, got {:?}",
19339                    self.peek()
19340                )));
19341            }
19342            self.advance();
19343            // Multi-arg unnest(a, b, …) zips the arrays in
19344            // parallel, NULL-padding to the longest (PG's ROWS
19345            // FROM shorthand). Lower onto the unnest channel as an
19346            // internal marker call the executors unpack.
19347            let expr = if srf_args.len() == 1 {
19348                srf_args.pop().expect("len checked")
19349            } else {
19350                crate::ast::Expr::FunctionCall {
19351                    name: "__unnest_zip".to_string(),
19352                    args: srf_args,
19353                }
19354            };
19355            let with_ordinality = self.absorb_with_ordinality();
19356            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19357            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19358            let correlated = Self::expr_has_any_column(&expr);
19359            let tref = TableRef {
19360                name,
19361                alias: alias_ident,
19362                only: false,
19363                as_of_segment: None,
19364                unnest_expr: Some(Box::new(expr)),
19365                unnest_column_aliases,
19366                with_ordinality,
19367                generate_series_args: None,
19368                lateral_subquery: None,
19369                jsonb_each_text_arg: None,
19370                table_fn_call: None,
19371                rows_from: None,
19372                json_table: None,
19373                scalar_fn_item: false,
19374            };
19375            return Ok(if correlated {
19376                Self::wrap_correlated_srf(tref)
19377            } else {
19378                tref
19379            });
19380        }
19381        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19382        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19383        // generic table-fn arg parser can't read), so it is intercepted
19384        // here BEFORE the generic dispatch. The doc expr may reference
19385        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19386        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19387                if s.eq_ignore_ascii_case("json_table"))
19388            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19389        {
19390            let tref = self.parse_json_table_ref()?;
19391            let correlated = tref
19392                .json_table
19393                .as_deref()
19394                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19395            return Ok(if correlated {
19396                Self::wrap_correlated_srf(tref)
19397            } else {
19398                tref
19399            });
19400        }
19401        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19402        // functions dispatched by name (`pg_partition_tree('t')`,
19403        // `pg_partition_ancestors('t')`). Same head-detection shape as
19404        // unnest; the engine executor owns the row shape per function.
19405        // v7.39 (read01 round 65) — and a USER function in FROM position
19406        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19407        // (generate_series / unnest / the json_each family) keep it — their arms
19408        // sit further down, so they are excluded here by name rather than by
19409        // ordering. Anything else that is an ident followed by `(` is a table
19410        // function; the engine executor decides whether it is a builtin, a
19411        // set-returning user function, or an error.
19412        // 7.38.1 S5.1 — pg_dump spells its table functions
19413        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19414        // strip the pg_catalog prefix here so the same head-detection
19415        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19416        // meaning.
19417        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19418            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19419            && matches!(
19420                self.tokens.get(self.pos + 2),
19421                Some(Token::Ident(_) | Token::QuotedIdent(_))
19422            )
19423            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19424        {
19425            self.advance(); // pg_catalog
19426            self.advance(); // .
19427        }
19428        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19429                if !s.eq_ignore_ascii_case("generate_series")
19430                    && !s.eq_ignore_ascii_case("unnest")
19431                    && !is_json_each_name(s))
19432            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19433        {
19434            // Body out-of-line — this parse sits on the FROM/subquery
19435            // recursion chain (debug frame-cliff discipline).
19436            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19437            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19438            // outer row, so it rides the lateral channel. Same rule the unnest
19439            // arm uses.
19440            let tref = self.parse_table_fn_ref()?;
19441            let correlated = tref
19442                .table_fn_call
19443                .as_deref()
19444                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19445            return Ok(if correlated {
19446                Self::wrap_correlated_srf(tref)
19447            } else {
19448                tref
19449            });
19450        }
19451        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19452        // [, step])` set-returning source. Same shape as unnest:
19453        // detect at the head, parse the comma-separated arg list,
19454        // dispatch downstream through the engine's set-returning
19455        // path. Supports integer triplets (mailrs's `WITH row_no AS
19456        // (SELECT * FROM generate_series(1, N))` pattern) and
19457        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19458        // date-range iteration pattern, which pre-3.10 had no
19459        // direct equivalent in SPG).
19460        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19461            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19462        {
19463            self.advance(); // generate_series
19464            self.advance(); // (
19465            let mut args: Vec<Expr> = Vec::new();
19466            loop {
19467                args.push(self.parse_expr(0)?);
19468                if matches!(self.peek(), Token::Comma) {
19469                    self.advance();
19470                    continue;
19471                }
19472                break;
19473            }
19474            if !matches!(self.peek(), Token::RParen) {
19475                return Err(self.err(alloc::format!(
19476                    "expected ')' after generate_series() arguments, got {:?}",
19477                    self.peek()
19478                )));
19479            }
19480            self.advance();
19481            if args.len() < 2 || args.len() > 3 {
19482                return Err(self.err(alloc::format!(
19483                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19484                    args.len()
19485                )));
19486            }
19487            let with_ordinality = self.absorb_with_ordinality();
19488            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19489            let name = alias_ident
19490                .clone()
19491                .unwrap_or_else(|| "generate_series".to_string());
19492            let correlated = args.iter().any(Self::expr_has_any_column);
19493            let tref = TableRef {
19494                name,
19495                alias: alias_ident,
19496                only: false,
19497                as_of_segment: None,
19498                unnest_expr: None,
19499                unnest_column_aliases: column_aliases,
19500                with_ordinality,
19501                generate_series_args: Some(args),
19502                lateral_subquery: None,
19503                jsonb_each_text_arg: None,
19504                table_fn_call: None,
19505                rows_from: None,
19506                json_table: None,
19507                scalar_fn_item: false,
19508            };
19509            return Ok(if correlated {
19510                Self::wrap_correlated_srf(tref)
19511            } else {
19512                tref
19513            });
19514        }
19515        // v7.16.2 — preserve information_schema / pg_catalog
19516        // qualifiers (mailrs round-10 A.3). The generic
19517        // `expect_ident_like` strip silently drops the schema;
19518        // we want the engine to recognise these PG meta tables
19519        // and synthesise rows from the live catalog. Produce a
19520        // synthetic name (`__spg_info_columns` etc.) so the
19521        // engine's SELECT-side router can dispatch without
19522        // clashing with any user-defined `columns` table.
19523        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19524            (synth, Some(orig))
19525        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19526            (synth, Some(orig))
19527        } else {
19528            (self.expect_ident_like()?, None)
19529        };
19530        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19531        // time-travel clause. Parse BEFORE the alias so the
19532        // alias can still ride at the tail (`tbl AS OF SEGMENT
19533        // '5' alias`). `AS` is a reserved keyword token, while
19534        // `OF` and `SEGMENT` are bare idents.
19535        let as_of_segment = if matches!(self.peek(), Token::As)
19536            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19537        {
19538            self.advance(); // AS
19539            self.advance(); // OF
19540            let kw = match self.peek().clone() {
19541                Token::Ident(s) | Token::QuotedIdent(s) => s,
19542                other => {
19543                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19544                }
19545            };
19546            if !kw.eq_ignore_ascii_case("segment") {
19547                return Err(self.err(format!(
19548                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19549                )));
19550            }
19551            self.advance();
19552            // Segment id literal — accept either a string or
19553            // integer for operator ergonomics.
19554            let id = match self.advance() {
19555                Token::String(s) => s
19556                    .parse::<u32>()
19557                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19558                Token::Integer(n) => u32::try_from(n)
19559                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19560                other => {
19561                    return Err(self.err(format!(
19562                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19563                    )));
19564                }
19565            };
19566            Some(id)
19567        } else {
19568            None
19569        };
19570        // TABLESAMPLE is not a reserved token — keep the bare-ident
19571        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19572        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19573        {
19574            None
19575        } else {
19576            self.parse_optional_alias()?
19577        };
19578        // r1052 — a catalog name rewritten to its synthetic form keeps
19579        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19580        // still binds after `pg_cast` became `__spg_pg_cast`. PG
19581        // semantics: the visible name of `pg_catalog.pg_cast` IS
19582        // `pg_cast`. Without this, every table-name-qualified column
19583        // on a synthesised catalog answered "missing FROM-clause
19584        // entry" — which is the wall pg_dump hit on its first
19585        // pg_proc/pg_cast query.
19586        let alias = match (&alias, &meta_original) {
19587            (None, Some(orig)) if *orig != name => Some(orig.clone()),
19588            _ => alias,
19589        };
19590        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19591        // (PG grammar). BERNOULLI lowers to a per-row
19592        // `random() < p/100` conjunct on the enclosing SELECT's
19593        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19594        // shares the lowering: SPG has no page structure to
19595        // sample, and the row-level form returns the same expected
19596        // fraction. REPEATABLE(seed) promises a deterministic
19597        // sample SPG cannot honour yet — honest error rather than
19598        // a silently ignored seed.
19599        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19600            self.advance();
19601            let method = self.expect_ident_like()?;
19602            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19603                return Err(self.err(alloc::format!(
19604                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19605                )));
19606            }
19607            if !matches!(self.peek(), Token::LParen) {
19608                return Err(self.err(alloc::format!(
19609                    "expected '(' after TABLESAMPLE {}, got {:?}",
19610                    method.to_ascii_uppercase(),
19611                    self.peek()
19612                )));
19613            }
19614            self.advance();
19615            let percent = self.parse_expr(0)?;
19616            if !matches!(self.peek(), Token::RParen) {
19617                return Err(self.err(alloc::format!(
19618                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19619                    self.peek()
19620                )));
19621            }
19622            self.advance();
19623            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19624            // `seed`, so the sample is stable across repeats and rescans.
19625            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19626            let mut sample_seed: Option<Expr> = None;
19627            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19628                self.advance();
19629                if !matches!(self.peek(), Token::LParen) {
19630                    return Err(self.err(alloc::format!(
19631                        "expected '(' after REPEATABLE, got {:?}",
19632                        self.peek()
19633                    )));
19634                }
19635                self.advance();
19636                let seed = self.parse_expr(0)?;
19637                if !matches!(self.peek(), Token::RParen) {
19638                    return Err(self.err(alloc::format!(
19639                        "expected ')' after REPEATABLE seed, got {:?}",
19640                        self.peek()
19641                    )));
19642                }
19643                self.advance();
19644                sample_seed = Some(seed);
19645            }
19646            let draw = match sample_seed {
19647                Some(seed) => Expr::FunctionCall {
19648                    name: "__tsm_fract".to_string(),
19649                    args: alloc::vec![seed],
19650                },
19651                None => Expr::FunctionCall {
19652                    name: "random".to_string(),
19653                    args: Vec::new(),
19654                },
19655            };
19656            self.pending_sample_preds.push(Expr::Binary {
19657                lhs: Box::new(draw),
19658                op: crate::ast::BinOp::Lt,
19659                rhs: Box::new(Expr::Binary {
19660                    lhs: Box::new(percent),
19661                    op: crate::ast::BinOp::Div,
19662                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19663                }),
19664            });
19665        }
19666        Ok(TableRef {
19667            name,
19668            alias,
19669            only,
19670            as_of_segment,
19671            unnest_expr: None,
19672            unnest_column_aliases: Vec::new(),
19673            with_ordinality: false,
19674            generate_series_args: None,
19675            lateral_subquery: None,
19676            jsonb_each_text_arg: None,
19677            table_fn_call: None,
19678            rows_from: None,
19679            json_table: None,
19680            scalar_fn_item: false,
19681        })
19682    }
19683
19684    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19685    /// but also accepts `AS alias(col [, col, …])` — the
19686    /// PG-standard table-function column-list form. The column
19687    /// list is only honoured when paired with `UNNEST(...)` in
19688    /// the parent; other call sites currently discard it.
19689    /// True when the expression tree contains a qualified column
19690    /// reference (`t.col`) — the syntactic marker that an SRF
19691    /// argument correlates with a preceding FROM item.
19692    fn expr_has_qualified_column(e: &Expr) -> bool {
19693        match e {
19694            Expr::Column(c) => c.qualifier.is_some(),
19695            Expr::Binary { lhs, rhs, .. } => {
19696                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19697            }
19698            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19699            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19700            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19701            Expr::Case {
19702                operand,
19703                branches,
19704                else_branch,
19705            } => {
19706                operand
19707                    .as_deref()
19708                    .is_some_and(Self::expr_has_qualified_column)
19709                    || branches.iter().any(|(w, t)| {
19710                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19711                    })
19712                    || else_branch
19713                        .as_deref()
19714                        .is_some_and(Self::expr_has_qualified_column)
19715            }
19716            _ => false,
19717        }
19718    }
19719
19720    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19721    /// counts a bare (unqualified) column. A set-returning function has no
19722    /// input columns of its own, so ANY column in its arguments is an outer
19723    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19724    fn expr_has_any_column(e: &Expr) -> bool {
19725        match e {
19726            Expr::Column(_) => true,
19727            Expr::Binary { lhs, rhs, .. } => {
19728                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19729            }
19730            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19731            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19732            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19733            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19734            // constructor or subscript fell to the `_ => false` arm, so
19735            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19736            // channel and the eager peer eval answered `column "x" does
19737            // not exist` (the substitution walker already recurses both
19738            // shapes; only this detector was blind to them).
19739            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19740            Expr::ArraySubscript { target, index } => {
19741                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19742            }
19743            Expr::Case {
19744                operand,
19745                branches,
19746                else_branch,
19747            } => {
19748                operand.as_deref().is_some_and(Self::expr_has_any_column)
19749                    || branches
19750                        .iter()
19751                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19752                    || else_branch
19753                        .as_deref()
19754                        .is_some_and(Self::expr_has_any_column)
19755            }
19756            _ => false,
19757        }
19758    }
19759
19760    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19761    /// `generate_series(1, t.n)`) into the lateral_subquery
19762    /// channel: `SELECT * FROM <srf>` executes per outer row with
19763    /// outer references substituted (v7.37.43-T4.5 machinery).
19764    /// Uncorrelated SRFs stay on their plain channels.
19765    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19766        let name = srf.name.clone();
19767        let alias = srf.alias.clone();
19768        let inner = crate::ast::SelectStatement {
19769            locking: None,
19770            ctes: Vec::new(),
19771            distinct: false,
19772            distinct_on: Vec::new(),
19773            items: alloc::vec![crate::ast::SelectItem::Wildcard],
19774            from: Some(crate::ast::FromClause {
19775                primary: srf,
19776                joins: Vec::new(),
19777            }),
19778            where_: None,
19779            group_by: None,
19780            group_by_all: false,
19781            having: None,
19782            unions: Vec::new(),
19783            order_by: Vec::new(),
19784            limit: None,
19785            offset: None,
19786            limit_with_ties: false,
19787            window_check_exprs: Vec::new(),
19788        };
19789        TableRef {
19790            name,
19791            alias,
19792            only: false,
19793            as_of_segment: None,
19794            unnest_expr: None,
19795            unnest_column_aliases: Vec::new(),
19796            with_ordinality: false,
19797            generate_series_args: None,
19798            lateral_subquery: Some(Box::new(inner)),
19799            jsonb_each_text_arg: None,
19800            table_fn_call: None,
19801            rows_from: None,
19802            json_table: None,
19803            scalar_fn_item: false,
19804        }
19805    }
19806
19807    /// True when the expression tree contains an unresolved
19808    /// `OVER w` marker (see parse_over_clause).
19809    fn expr_has_named_window(e: &Expr) -> bool {
19810        match e {
19811            Expr::WindowFunction { partition_by, .. } => matches!(
19812                partition_by.as_slice(),
19813                [Expr::Column(c)] if matches!(
19814                    c.qualifier.as_deref(),
19815                    Some("__named_window__") | Some("__named_window_ref__")
19816                )
19817            ),
19818            Expr::Binary { lhs, rhs, .. } => {
19819                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19820            }
19821            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19822            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19823            Expr::Case {
19824                operand,
19825                branches,
19826                else_branch,
19827            } => {
19828                operand.as_deref().is_some_and(Self::expr_has_named_window)
19829                    || branches.iter().any(|(w, t)| {
19830                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19831                    })
19832                    || else_branch
19833                        .as_deref()
19834                        .is_some_and(Self::expr_has_named_window)
19835            }
19836            _ => false,
19837        }
19838    }
19839
19840    /// v7.39 (round 705) — the NAMES the expression references through the
19841    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19842    /// definitions nothing referenced. Traversal mirrors
19843    /// `expr_has_named_window` above.
19844    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19845        match e {
19846            Expr::WindowFunction { partition_by, .. } => {
19847                if let [Expr::Column(c)] = partition_by.as_slice()
19848                    && matches!(
19849                        c.qualifier.as_deref(),
19850                        Some("__named_window__") | Some("__named_window_ref__")
19851                    )
19852                {
19853                    into.push(c.name.clone());
19854                }
19855            }
19856            Expr::Binary { lhs, rhs, .. } => {
19857                Self::collect_named_window_refs(lhs, into);
19858                Self::collect_named_window_refs(rhs, into);
19859            }
19860            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19861                Self::collect_named_window_refs(expr, into);
19862            }
19863            Expr::FunctionCall { args, .. } => {
19864                for a in args {
19865                    Self::collect_named_window_refs(a, into);
19866                }
19867            }
19868            Expr::Case {
19869                operand,
19870                branches,
19871                else_branch,
19872            } => {
19873                if let Some(o) = operand.as_deref() {
19874                    Self::collect_named_window_refs(o, into);
19875                }
19876                for (w, t) in branches {
19877                    Self::collect_named_window_refs(w, into);
19878                    Self::collect_named_window_refs(t, into);
19879                }
19880                if let Some(eb) = else_branch.as_deref() {
19881                    Self::collect_named_window_refs(eb, into);
19882                }
19883            }
19884            _ => {}
19885        }
19886    }
19887
19888    /// Inline named-window definitions into the `OVER w` markers.
19889    /// An unknown name errors (PG: window "w" does not exist).
19890    #[allow(clippy::type_complexity)]
19891    fn substitute_named_windows(
19892        e: &mut Expr,
19893        defs: &[(
19894            String,
19895            (
19896                Vec<Expr>,
19897                Vec<(Expr, bool, Option<bool>)>,
19898                Option<WindowFrame>,
19899            ),
19900        )],
19901    ) -> Result<(), String> {
19902        match e {
19903            Expr::WindowFunction {
19904                partition_by,
19905                order_by,
19906                frame,
19907                ..
19908            } => {
19909                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19910                // from the bare `OVER w1` (a plain reference).
19911                let named = match partition_by.as_slice() {
19912                    [Expr::Column(c)] => match c.qualifier.as_deref() {
19913                        Some("__named_window__") => Some((c.name.clone(), false)),
19914                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
19915                        _ => None,
19916                    },
19917                    _ => None,
19918                };
19919                if let Some((wname, is_copy)) = named {
19920                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19921                    else {
19922                        return Err(alloc::format!("window {wname:?} does not exist"));
19923                    };
19924                    if !is_copy {
19925                        *partition_by = def.0.clone();
19926                        *order_by = def.1.clone();
19927                        *frame = def.2.clone();
19928                        return Ok(());
19929                    }
19930                    // v7.39 (round 229) — PG's copy rules, probed against
19931                    // 18.4: a copy inherits the partitioning, may supply an
19932                    // ordering only when the base has none, and may not copy
19933                    // a base that already carries a frame (its own frame
19934                    // would be ambiguous with the inherited one).
19935                    if !def.1.is_empty() && !order_by.is_empty() {
19936                        return Err(alloc::format!(
19937                            "cannot override ORDER BY clause of window \"{wname}\""
19938                        ));
19939                    }
19940                    if def.2.is_some() {
19941                        return Err(alloc::format!(
19942                            "cannot copy window \"{wname}\" because it has a frame clause"
19943                        ));
19944                    }
19945                    *partition_by = def.0.clone();
19946                    if order_by.is_empty() {
19947                        *order_by = def.1.clone();
19948                    }
19949                }
19950                Ok(())
19951            }
19952            Expr::Binary { lhs, rhs, .. } => {
19953                Self::substitute_named_windows(lhs, defs)?;
19954                Self::substitute_named_windows(rhs, defs)
19955            }
19956            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19957                Self::substitute_named_windows(expr, defs)
19958            }
19959            Expr::FunctionCall { args, .. } => {
19960                for a in args {
19961                    Self::substitute_named_windows(a, defs)?;
19962                }
19963                Ok(())
19964            }
19965            Expr::Case {
19966                operand,
19967                branches,
19968                else_branch,
19969            } => {
19970                if let Some(op) = operand {
19971                    Self::substitute_named_windows(op, defs)?;
19972                }
19973                for (w, t) in branches {
19974                    Self::substitute_named_windows(w, defs)?;
19975                    Self::substitute_named_windows(t, defs)?;
19976                }
19977                if let Some(el) = else_branch {
19978                    Self::substitute_named_windows(el, defs)?;
19979                }
19980                Ok(())
19981            }
19982            _ => Ok(()),
19983        }
19984    }
19985
19986    /// SQL-standard `TABLE name` shorthand — builds the equivalent
19987    /// `SELECT * FROM name` head. Callers own set-op chain / tail
19988    /// composition.
19989    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19990        debug_assert!(matches!(self.peek(), Token::Table));
19991        self.advance(); // TABLE
19992        let tname = self.expect_ident_like()?;
19993        Ok(SelectStatement {
19994            locking: None,
19995            ctes: Vec::new(),
19996            distinct: false,
19997            distinct_on: Vec::new(),
19998            items: alloc::vec![SelectItem::Wildcard],
19999            from: Some(FromClause {
20000                primary: TableRef {
20001                    name: tname,
20002                    alias: None,
20003                    only: false,
20004                    as_of_segment: None,
20005                    unnest_expr: None,
20006                    unnest_column_aliases: Vec::new(),
20007                    with_ordinality: false,
20008                    generate_series_args: None,
20009                    lateral_subquery: None,
20010                    jsonb_each_text_arg: None,
20011                    table_fn_call: None,
20012                    rows_from: None,
20013                    json_table: None,
20014                    scalar_fn_item: false,
20015                },
20016                joins: Vec::new(),
20017            }),
20018            where_: None,
20019            group_by: None,
20020            group_by_all: false,
20021            having: None,
20022            unions: Vec::new(),
20023            order_by: Vec::new(),
20024            limit: None,
20025            offset: None,
20026            limit_with_ties: false,
20027            window_check_exprs: Vec::new(),
20028        })
20029    }
20030
20031    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20032    /// variants) → a derived table that reads each declared column out of
20033    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20034    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20035    /// the scalar *record form projects a single row straight off `J`.
20036    /// Rides the existing lateral-subquery channel, so no new executor or
20037    /// AST is needed.
20038    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20039        use crate::ast::{
20040            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20041        };
20042        let fn_name = match self.peek() {
20043            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20044            _ => unreachable!("caller guarded is_json_to_record_name"),
20045        };
20046        self.advance(); // fn name
20047        self.advance(); // (
20048        let mut arg = self.parse_expr(0)?;
20049        // populate_record(base, json): the base only carries the record
20050        // type here — the JSON argument is the second expression.
20051        let mut base: Option<Expr> = None;
20052        if matches!(self.peek(), Token::Comma) {
20053            self.advance();
20054            base = Some(arg);
20055            arg = self.parse_expr(0)?;
20056        }
20057        if !matches!(self.peek(), Token::RParen) {
20058            return Err(self.err(alloc::format!(
20059                "expected ')' after {fn_name}() argument, got {:?}",
20060                self.peek()
20061            )));
20062        }
20063        self.advance(); // )
20064        let is_set = fn_name.ends_with("recordset");
20065        // `[AS] alias ( col type [, …] )` column-definition list.
20066        if matches!(self.peek(), Token::As) {
20067            self.advance();
20068        }
20069        let alias_opt = match self.peek() {
20070            Token::Ident(s) | Token::QuotedIdent(s) => {
20071                let a = s.clone();
20072                self.advance();
20073                Some(a)
20074            }
20075            _ => None,
20076        };
20077        // v7.39 (read01 round 76) — the populate family's canonical PG
20078        // spelling carries no column list at all: the row shape comes from
20079        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20080        // j)`). The parser has no catalog, so hand the two arguments to the
20081        // engine's table-function channel, which does. Only `*_to_record*`
20082        // (whose base is bare `record`) genuinely requires the list.
20083        if !matches!(self.peek(), Token::LParen) {
20084            if let Some(base_expr) = base {
20085                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20086                return Ok(TableRef {
20087                    name: alias.clone(),
20088                    alias: Some(alias),
20089                    only: false,
20090                    as_of_segment: None,
20091                    unnest_expr: None,
20092                    unnest_column_aliases: Vec::new(),
20093                    with_ordinality: false,
20094                    generate_series_args: None,
20095                    lateral_subquery: None,
20096                    jsonb_each_text_arg: None,
20097                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20098                    rows_from: None,
20099                    json_table: None,
20100                    scalar_fn_item: false,
20101                });
20102            }
20103            return Err(self.err(alloc::format!(
20104                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20105                self.peek()
20106            )));
20107        }
20108        let Some(alias) = alias_opt else {
20109            return Err(self.err(alloc::format!(
20110                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20111            )));
20112        };
20113        self.advance(); // (
20114        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20115        loop {
20116            let col = self.expect_ident_like()?;
20117            let ty = self.parse_cast_target()?;
20118            coldefs.push((col, ty));
20119            if matches!(self.peek(), Token::Comma) {
20120                self.advance();
20121                continue;
20122            }
20123            if matches!(self.peek(), Token::RParen) {
20124                self.advance();
20125                break;
20126            }
20127            return Err(self.err(alloc::format!(
20128                "expected ',' or ')' in {fn_name} column list, got {:?}",
20129                self.peek()
20130            )));
20131        }
20132        if coldefs.is_empty() {
20133            return Err(self.err(alloc::format!(
20134                "{fn_name} column-definition list must declare at least one column"
20135            )));
20136        }
20137        // Per column: (base ->> 'col')::type AS col. The base is the
20138        // per-element `value` column for the *set form, or the argument
20139        // itself for the scalar record form.
20140        let items: Vec<SelectItem> = coldefs
20141            .into_iter()
20142            .map(|(col, ty)| {
20143                let base = if is_set {
20144                    Expr::Column(ColumnName {
20145                        qualifier: None,
20146                        name: "value".to_string(),
20147                    })
20148                } else {
20149                    arg.clone()
20150                };
20151                SelectItem::Expr {
20152                    expr: Expr::Cast {
20153                        expr: Box::new(Expr::Binary {
20154                            lhs: Box::new(base),
20155                            op: BinOp::JsonGetText,
20156                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20157                        }),
20158                        target: ty,
20159                    },
20160                    alias: Some(col),
20161                }
20162            })
20163            .collect();
20164        let from = if is_set {
20165            let elem_fn = if fn_name.starts_with("jsonb") {
20166                "jsonb_array_elements"
20167            } else {
20168                "json_array_elements"
20169            };
20170            Some(FromClause {
20171                primary: TableRef {
20172                    name: "value".to_string(),
20173                    alias: None,
20174                    only: false,
20175                    as_of_segment: None,
20176                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20177                        name: elem_fn.to_string(),
20178                        args: alloc::vec![arg],
20179                    })),
20180                    unnest_column_aliases: alloc::vec!["value".to_string()],
20181                    with_ordinality: false,
20182                    generate_series_args: None,
20183                    lateral_subquery: None,
20184                    jsonb_each_text_arg: None,
20185                    table_fn_call: None,
20186                    rows_from: None,
20187                    json_table: None,
20188                    scalar_fn_item: false,
20189                },
20190                joins: Vec::new(),
20191            })
20192        } else {
20193            None
20194        };
20195        let inner = SelectStatement {
20196            locking: None,
20197            ctes: Vec::new(),
20198            distinct: false,
20199            distinct_on: Vec::new(),
20200            items,
20201            from,
20202            where_: None,
20203            group_by: None,
20204            group_by_all: false,
20205            having: None,
20206            unions: Vec::new(),
20207            order_by: Vec::new(),
20208            limit: None,
20209            offset: None,
20210            limit_with_ties: false,
20211            window_check_exprs: Vec::new(),
20212        };
20213        Ok(TableRef {
20214            name: alias.clone(),
20215            alias: Some(alias),
20216            only: false,
20217            as_of_segment: None,
20218            unnest_expr: None,
20219            unnest_column_aliases: Vec::new(),
20220            with_ordinality: false,
20221            generate_series_args: None,
20222            lateral_subquery: Some(Box::new(inner)),
20223            jsonb_each_text_arg: None,
20224            table_fn_call: None,
20225            rows_from: None,
20226            json_table: None,
20227            scalar_fn_item: false,
20228        })
20229    }
20230
20231    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20232    /// Returns true when the clause was present. `WITH` alone (a
20233    /// CTE can never start here) is not enough — the ORDINALITY
20234    /// ident must follow, so a stray WITH still errors downstream.
20235    fn absorb_with_ordinality(&mut self) -> bool {
20236        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20237            && matches!(self.tokens.get(self.pos + 1),
20238                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20239        {
20240            self.advance();
20241            self.advance();
20242            true
20243        } else {
20244            false
20245        }
20246    }
20247
20248    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20249    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20250    /// Out-of-line: the caller sits on the FROM recursion chain.
20251    #[inline(never)]
20252    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20253        let fn_name = match self.advance() {
20254            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20255            _ => unreachable!("caller peeked an ident"),
20256        };
20257        self.advance(); // (
20258        let mut args: Vec<Expr> = Vec::new();
20259        if !matches!(self.peek(), Token::RParen) {
20260            loop {
20261                args.push(self.parse_expr(0)?);
20262                if matches!(self.peek(), Token::Comma) {
20263                    self.advance();
20264                    continue;
20265                }
20266                break;
20267            }
20268        }
20269        if !matches!(self.peek(), Token::RParen) {
20270            return Err(self.err(alloc::format!(
20271                "expected ')' after {fn_name}() arguments, got {:?}",
20272                self.peek()
20273            )));
20274        }
20275        self.advance();
20276        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20277        // counter column rides after the function's own, and the alias list
20278        // names it.
20279        let with_ordinality = self.absorb_with_ordinality();
20280        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20281        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20282        Ok(TableRef {
20283            name,
20284            alias: alias_ident,
20285            only: false,
20286            as_of_segment: None,
20287            unnest_expr: None,
20288            unnest_column_aliases,
20289            with_ordinality,
20290            generate_series_args: None,
20291            lateral_subquery: None,
20292            jsonb_each_text_arg: None,
20293            table_fn_call: Some(Box::new((fn_name, args))),
20294            rows_from: None,
20295            json_table: None,
20296            scalar_fn_item: false,
20297        })
20298    }
20299
20300    /// v7.39 (round 205, JSON_TABLE) — parse
20301    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20302    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20303    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20304    #[inline(never)]
20305    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20306        self.advance(); // json_table
20307        self.advance(); // (
20308        let doc = Box::new(self.parse_expr(0)?);
20309        self.expect_comma_json_table()?;
20310        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20311        // Optional `PASSING <expr> AS <name> [, …]`.
20312        let mut passing: Vec<(String, Expr)> = Vec::new();
20313        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20314            self.advance();
20315            loop {
20316                let e = self.parse_expr(0)?;
20317                if !matches!(self.peek(), Token::As) {
20318                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20319                }
20320                self.advance();
20321                let vname = match self.advance() {
20322                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20323                    other => {
20324                        return Err(self.err(alloc::format!(
20325                            "expected PASSING variable name, got {other:?}"
20326                        )));
20327                    }
20328                };
20329                passing.push((vname, e));
20330                if matches!(self.peek(), Token::Comma) {
20331                    self.advance();
20332                    continue;
20333                }
20334                break;
20335            }
20336        }
20337        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20338            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20339        }
20340        self.advance();
20341        let columns = self.parse_json_table_columns()?;
20342        if !matches!(self.peek(), Token::RParen) {
20343            return Err(self.err(alloc::format!(
20344                "expected ')' to close JSON_TABLE, got {:?}",
20345                self.peek()
20346            )));
20347        }
20348        self.advance();
20349        let alias_ident = self.parse_optional_alias()?;
20350        let name = alias_ident
20351            .clone()
20352            .unwrap_or_else(|| String::from("json_table"));
20353        Ok(TableRef {
20354            name,
20355            alias: alias_ident,
20356            only: false,
20357            as_of_segment: None,
20358            unnest_expr: None,
20359            unnest_column_aliases: Vec::new(),
20360            with_ordinality: false,
20361            generate_series_args: None,
20362            lateral_subquery: None,
20363            jsonb_each_text_arg: None,
20364            table_fn_call: None,
20365            rows_from: None,
20366            json_table: Some(Box::new(crate::ast::JsonTable {
20367                doc,
20368                row_path,
20369                columns,
20370                passing,
20371            })),
20372            scalar_fn_item: false,
20373        })
20374    }
20375
20376    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20377        if !matches!(self.peek(), Token::Comma) {
20378            return Err(self.err(alloc::format!(
20379                "expected ',' after JSON_TABLE document, got {:?}",
20380                self.peek()
20381            )));
20382        }
20383        self.advance();
20384        Ok(())
20385    }
20386
20387    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20388        match self.advance() {
20389            Token::String(s) => Ok(s),
20390            other => Err(self.err(alloc::format!(
20391                "expected {what} string literal, got {other:?}"
20392            ))),
20393        }
20394    }
20395
20396    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20397    #[inline(never)]
20398    fn parse_json_table_columns(
20399        &mut self,
20400    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20401        if !matches!(self.peek(), Token::LParen) {
20402            return Err(self.err("expected '(' after COLUMNS".into()));
20403        }
20404        self.advance();
20405        let mut cols = Vec::new();
20406        loop {
20407            cols.push(self.parse_json_table_one_column()?);
20408            if matches!(self.peek(), Token::Comma) {
20409                self.advance();
20410                continue;
20411            }
20412            break;
20413        }
20414        if !matches!(self.peek(), Token::RParen) {
20415            return Err(self.err(alloc::format!(
20416                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20417                self.peek()
20418            )));
20419        }
20420        self.advance();
20421        Ok(cols)
20422    }
20423
20424    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20425        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20426        // NESTED [PATH] '<p>' COLUMNS (...)
20427        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20428            self.advance();
20429            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20430                self.advance();
20431            }
20432            let path = self.parse_json_string_literal("NESTED PATH")?;
20433            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20434                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20435            }
20436            self.advance();
20437            let columns = self.parse_json_table_columns()?;
20438            return Ok(JsonTableColumn::Nested { path, columns });
20439        }
20440        // <name> ...
20441        let name = match self.advance() {
20442            Token::Ident(s) | Token::QuotedIdent(s) => s,
20443            other => {
20444                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20445            }
20446        };
20447        // <name> FOR ORDINALITY
20448        if matches!(self.peek(), Token::For) {
20449            self.advance();
20450            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20451                return Err(self.err("expected ORDINALITY after FOR".into()));
20452            }
20453            self.advance();
20454            return Ok(JsonTableColumn::Ordinality { name });
20455        }
20456        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20457        let ty = self.parse_column_type_name()?;
20458        let mut format_json = false;
20459        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20460            self.advance();
20461            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20462                return Err(self.err("expected JSON after FORMAT".into()));
20463            }
20464            self.advance();
20465            format_json = true;
20466        }
20467        let mut exists = false;
20468        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20469            self.advance();
20470            exists = true;
20471        }
20472        let mut path = alloc::format!("$.{name}");
20473        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20474            self.advance();
20475            path = self.parse_json_string_literal("column PATH")?;
20476        }
20477        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20478            // `FORMAT JSON` after PATH (alternate placement).
20479            self.advance();
20480            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20481                self.advance();
20482            }
20483            format_json = true;
20484        }
20485        let mut wrapper = false;
20486        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20487            self.advance();
20488            // optional CONDITIONAL/UNCONDITIONAL
20489            if matches!(self.peek(), Token::Ident(s)
20490                if s.eq_ignore_ascii_case("unconditional")
20491                    || s.eq_ignore_ascii_case("conditional"))
20492            {
20493                self.advance();
20494            }
20495            if !matches!(self.peek(), Token::Ident(s)
20496                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20497            {
20498                return Err(self.err("expected WRAPPER after WITH".into()));
20499            }
20500            self.advance();
20501            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20502            if matches!(self.peek(), Token::Ident(s)
20503                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20504            {
20505                self.advance();
20506            }
20507            wrapper = true;
20508        }
20509        // ON EMPTY / ON ERROR clauses (two, in any order).
20510        let mut on_empty = JsonTableOnBehavior::Null;
20511        let mut on_error = JsonTableOnBehavior::Null;
20512        for _ in 0..2 {
20513            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20514            {
20515                self.advance();
20516                Some(JsonTableOnBehavior::Error)
20517            } else if matches!(self.peek(), Token::Null) {
20518                self.advance();
20519                Some(JsonTableOnBehavior::Null)
20520            } else if matches!(self.peek(), Token::Default) {
20521                self.advance();
20522                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20523            } else {
20524                None
20525            };
20526            let Some(behavior) = behavior else { break };
20527            // `ON {EMPTY|ERROR}`
20528            if !matches!(self.peek(), Token::On) {
20529                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20530            }
20531            self.advance();
20532            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20533                self.advance();
20534                on_empty = behavior;
20535            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20536                self.advance();
20537                on_error = behavior;
20538            } else {
20539                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20540            }
20541        }
20542        Ok(JsonTableColumn::Regular {
20543            name,
20544            ty,
20545            path,
20546            exists,
20547            format_json,
20548            wrapper,
20549            on_empty,
20550            on_error,
20551        })
20552    }
20553
20554    fn parse_optional_alias_with_columns(
20555        &mut self,
20556    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20557        let alias = self.parse_optional_alias()?;
20558        if alias.is_none() {
20559            return Ok((None, Vec::new()));
20560        }
20561        let mut cols: Vec<String> = Vec::new();
20562        if matches!(self.peek(), Token::LParen) {
20563            self.advance();
20564            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20565                self.advance();
20566                cols.push(s);
20567                if matches!(self.peek(), Token::Comma) {
20568                    self.advance();
20569                    continue;
20570                }
20571                break;
20572            }
20573            if matches!(self.peek(), Token::RParen) {
20574                self.advance();
20575            }
20576        }
20577        Ok((alias, cols))
20578    }
20579
20580    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20581    /// whose keyword token was already consumed and whose `(` is the
20582    /// current token. Factored out of `parse_atom` (and marked
20583    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20584    /// recursive `parse_atom` frame — inlining them there enlarges the
20585    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20586    /// against, risking an overflow before the budget triggers.
20587    #[inline(never)]
20588    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20589        self.advance(); // (
20590        let mut args = Vec::new();
20591        if !matches!(self.peek(), Token::RParen) {
20592            loop {
20593                args.push(self.parse_expr(0)?);
20594                match self.peek() {
20595                    Token::Comma => {
20596                        self.advance();
20597                    }
20598                    Token::RParen => break,
20599                    other => {
20600                        return Err(self.err(alloc::format!(
20601                            "expected ',' or ')' in {name}() args, got {other:?}"
20602                        )));
20603                    }
20604                }
20605            }
20606        }
20607        self.advance(); // )
20608        Ok(Expr::FunctionCall {
20609            name: name.into(),
20610            args,
20611        })
20612    }
20613
20614    /// FROM-clause: a primary table reference plus zero-or-more joined
20615    /// peers expressed via either `, <table>` (cross-product, no ON) or
20616    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20617    /// v1.10 keeps the join list flat (left-associative nested-loop
20618    /// semantics).
20619    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20620        let primary = self.parse_table_ref()?;
20621        let primary_qual = primary
20622            .alias
20623            .clone()
20624            .unwrap_or_else(|| primary.name.clone());
20625        let joins = self.parse_from_joins(&primary_qual)?;
20626        Ok(FromClause { primary, joins })
20627    }
20628
20629    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20630    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20631    /// SAME grammar after its target table has already been consumed.
20632    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20633    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20634    /// be parsed forward, once.)
20635    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20636    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20637    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20638    /// desugaring, which needs a name for the left side of each equality.
20639    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20640        let mut joins = Vec::new();
20641        loop {
20642            // `, <table>` — cross-product with no ON.
20643            if matches!(self.peek(), Token::Comma) {
20644                self.advance();
20645                let table = self.parse_table_ref()?;
20646                joins.push(FromJoin {
20647                    kind: JoinKind::Cross,
20648                    table,
20649                    on: None,
20650                    using_cols: None,
20651                    natural: false,
20652                });
20653                continue;
20654            }
20655            // v7.37.16 — optional leading `NATURAL` before the join
20656            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20657            // not a lexer keyword (it arrives as a bare Ident), so match
20658            // it case-insensitively here. When present, no ON/USING
20659            // clause is allowed — the common columns are resolved at
20660            // execution time.
20661            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20662            if natural {
20663                self.advance();
20664            }
20665            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20666            // CROSS JOIN, and bare JOIN (defaults to INNER).
20667            let kind =
20668                match self.peek() {
20669                    Token::Inner => {
20670                        self.advance();
20671                        if !matches!(self.peek(), Token::Join) {
20672                            return Err(self
20673                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20674                        }
20675                        self.advance();
20676                        JoinKind::Inner
20677                    }
20678                    Token::Left => {
20679                        self.advance();
20680                        if matches!(self.peek(), Token::Outer) {
20681                            self.advance();
20682                        }
20683                        if !matches!(self.peek(), Token::Join) {
20684                            return Err(self.err(format!(
20685                                "expected JOIN after LEFT [OUTER], got {:?}",
20686                                self.peek()
20687                            )));
20688                        }
20689                        self.advance();
20690                        JoinKind::Left
20691                    }
20692                    Token::Cross => {
20693                        self.advance();
20694                        if !matches!(self.peek(), Token::Join) {
20695                            return Err(self
20696                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20697                        }
20698                        self.advance();
20699                        JoinKind::Cross
20700                    }
20701                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20702                    Token::Right => {
20703                        self.advance();
20704                        if matches!(self.peek(), Token::Outer) {
20705                            self.advance();
20706                        }
20707                        if !matches!(self.peek(), Token::Join) {
20708                            return Err(self.err(format!(
20709                                "expected JOIN after RIGHT [OUTER], got {:?}",
20710                                self.peek()
20711                            )));
20712                        }
20713                        self.advance();
20714                        JoinKind::Right
20715                    }
20716                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20717                    Token::Full => {
20718                        self.advance();
20719                        if matches!(self.peek(), Token::Outer) {
20720                            self.advance();
20721                        }
20722                        if !matches!(self.peek(), Token::Join) {
20723                            return Err(self.err(format!(
20724                                "expected JOIN after FULL [OUTER], got {:?}",
20725                                self.peek()
20726                            )));
20727                        }
20728                        self.advance();
20729                        JoinKind::FullOuter
20730                    }
20731                    Token::Join => {
20732                        self.advance();
20733                        JoinKind::Inner
20734                    }
20735                    _ => break,
20736                };
20737            let table = self.parse_table_ref()?;
20738            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20739            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20740            // where prev_table is the most-recent left-side table
20741            // (the previous join's table if any, else the FROM primary).
20742            // PG semantics around column merging are richer (USING'd
20743            // cols become deduplicated single output columns); for
20744            // sugar purposes the predicate-only form covers the
20745            // baseline corpus shape and chained `… JOIN x USING (k)
20746            // JOIN y USING (k)` calls.
20747            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20748            // common columns resolve at execution time.
20749            if natural {
20750                joins.push(FromJoin {
20751                    kind,
20752                    table,
20753                    on: None,
20754                    using_cols: None,
20755                    natural: true,
20756                });
20757                continue;
20758            }
20759            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20760            // v7.37.16 — capture the USING column list (in addition to
20761            // the ON desugar below) so the executor can perform PG's
20762            // column-merge on the output side.
20763            let mut using_cols: Option<Vec<String>> = None;
20764            let on = if matches!(self.peek(), Token::On) {
20765                self.advance();
20766                Some(self.parse_expr(0)?)
20767            } else if using_match {
20768                self.advance();
20769                if !matches!(self.peek(), Token::LParen) {
20770                    return Err(
20771                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20772                    );
20773                }
20774                self.advance();
20775                let mut cols: Vec<String> = Vec::new();
20776                loop {
20777                    match self.peek().clone() {
20778                        Token::Ident(s) | Token::QuotedIdent(s) => {
20779                            self.advance();
20780                            cols.push(s);
20781                        }
20782                        other => {
20783                            return Err(self.err(format!(
20784                                "expected column name inside USING (…), got {other:?}"
20785                            )));
20786                        }
20787                    }
20788                    match self.peek() {
20789                        Token::Comma => {
20790                            self.advance();
20791                            continue;
20792                        }
20793                        Token::RParen => {
20794                            self.advance();
20795                            break;
20796                        }
20797                        other => {
20798                            return Err(self.err(format!(
20799                                "expected ',' or ')' inside USING (…), got {other:?}"
20800                            )));
20801                        }
20802                    }
20803                }
20804                if cols.is_empty() {
20805                    return Err(self.err("USING (…) requires at least one column".to_string()));
20806                }
20807                using_cols = Some(cols.clone());
20808                // Pick the left-side alias: prev join's table if any,
20809                // else FROM primary. Use alias when present, else
20810                // table name (PG-equivalent qualifier).
20811                let left_qual: String = joins
20812                    .last()
20813                    .map(|j| {
20814                        j.table
20815                            .alias
20816                            .clone()
20817                            .unwrap_or_else(|| j.table.name.clone())
20818                    })
20819                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20820                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20821                let mut iter = cols.into_iter().map(|c| Expr::Binary {
20822                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20823                        qualifier: Some(left_qual.clone()),
20824                        name: c.clone(),
20825                    })),
20826                    op: crate::ast::BinOp::Eq,
20827                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20828                        qualifier: Some(right_qual.clone()),
20829                        name: c,
20830                    })),
20831                });
20832                let first = iter.next().expect("at least one col");
20833                Some(iter.fold(first, |acc, pred| Expr::Binary {
20834                    lhs: alloc::boxed::Box::new(acc),
20835                    op: crate::ast::BinOp::And,
20836                    rhs: alloc::boxed::Box::new(pred),
20837                }))
20838            } else if kind == JoinKind::Cross {
20839                None
20840            } else {
20841                return Err(self.err(format!(
20842                    "expected ON or USING after {:?} JOIN, got {:?}",
20843                    kind,
20844                    self.peek()
20845                )));
20846            };
20847            joins.push(FromJoin {
20848                kind,
20849                table,
20850                on,
20851                using_cols,
20852                natural: false,
20853            });
20854        }
20855        Ok(joins)
20856    }
20857
20858    /// Optional alias after an expression or table:
20859    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20860    /// accepted (PG-style implicit alias). Returns `None` if the next token
20861    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20862    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20863        if matches!(self.peek(), Token::As) {
20864            self.advance();
20865            // v7.39 (round 340, V56) — after AS the next token MUST be an
20866            // identifier. This used to return None and "let the caller
20867            // surface the error on the next expectation", but when AS is
20868            // the LAST token there is no next expectation: `SELECT 1 AS`
20869            // parsed clean and silently dropped the alias. PG rejects it.
20870            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20871                return self.expect_ident_like().map(Some);
20872            }
20873            return Err(self.err(alloc::format!(
20874                "expected an alias after AS, got {:?}",
20875                self.peek()
20876            )));
20877        }
20878        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20879        // grammar reserves a long list of follow-keywords from the
20880        // alias slot. SPG's bareword approximation: skip a small
20881        // set of idents that would otherwise be swallowed as the
20882        // table alias and break trailing clauses like CREATE
20883        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20884        // CONFLICT WHERE shapes.
20885        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20886            if is_alias_stopword(s) {
20887                return Ok(None);
20888            }
20889            return Ok(self.expect_ident_like().ok());
20890        }
20891        Ok(None)
20892    }
20893
20894    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20895    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20896        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20897        // error beats a stack overflow (an overflow aborts the
20898        // embedding host process).
20899        self.enter_nested()?;
20900        let r = self.parse_expr_inner(min_prec);
20901        self.nest_depth -= 1;
20902        r
20903    }
20904
20905    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20906    /// When the upcoming tokens form one, return the underlying
20907    /// operator token and the position just past the closing paren
20908    /// so the binary loop can dispatch on the plain operator.
20909    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20910        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20911            return None;
20912        }
20913        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20914            return None;
20915        }
20916        let mut i = self.pos + 2;
20917        // Optional schema qualifier (pg_catalog.<op> etc.).
20918        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20919            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20920        {
20921            i += 2;
20922        }
20923        let op_tok = self.tokens.get(i)?.clone();
20924        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20925            return None;
20926        }
20927        Some((i + 2, op_tok))
20928    }
20929
20930    /// PG operator symbols that lower onto function calls in
20931    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20932    /// family → regexp_like, comparison rung), `^@` (starts_with,
20933    /// comparison rung), `^` (power, tighter than `*`), `#`
20934    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20935    /// subset of the OR bits so the subtraction never borrows).
20936    fn try_symbol_operator(
20937        &mut self,
20938        lhs: &Expr,
20939        min_prec: u8,
20940    ) -> Result<Option<Expr>, ParseError> {
20941        enum Sym {
20942            Regex { ci: bool, negated: bool },
20943            Like { ci: bool, negated: bool },
20944            StartsWith,
20945            Power,
20946            Xor,
20947            RangeAdjacent,
20948        }
20949        // v7.39 (IS-precedence knife) — the low-precedence postfix
20950        // predicates ride this existing leaf call (zero new frame slots
20951        // on the nesting chain).
20952        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20953            return Ok(Some(e));
20954        }
20955        let (sym, prec): (Sym, u8) = match self.peek() {
20956            Token::Tilde => (
20957                Sym::Regex {
20958                    ci: false,
20959                    negated: false,
20960                },
20961                5,
20962            ),
20963            Token::TildeStar => (
20964                Sym::Regex {
20965                    ci: true,
20966                    negated: false,
20967                },
20968                5,
20969            ),
20970            Token::NotTilde => (
20971                Sym::Regex {
20972                    ci: false,
20973                    negated: true,
20974                },
20975                5,
20976            ),
20977            Token::NotTildeStar => (
20978                Sym::Regex {
20979                    ci: true,
20980                    negated: true,
20981                },
20982                5,
20983            ),
20984            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20985            Token::DoubleTilde => (
20986                Sym::Like {
20987                    ci: false,
20988                    negated: false,
20989                },
20990                5,
20991            ),
20992            Token::DoubleTildeStar => (
20993                Sym::Like {
20994                    ci: true,
20995                    negated: false,
20996                },
20997                5,
20998            ),
20999            Token::NotDoubleTilde => (
21000                Sym::Like {
21001                    ci: false,
21002                    negated: true,
21003                },
21004                5,
21005            ),
21006            Token::NotDoubleTildeStar => (
21007                Sym::Like {
21008                    ci: true,
21009                    negated: true,
21010                },
21011                5,
21012            ),
21013            Token::CaretAt => (Sym::StartsWith, 5),
21014            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21015            // tighter than `* / & |`, which the prec-9 rung preserves —
21016            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21017            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21018            Token::Caret => (Sym::Power, 9),
21019            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21020            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21021            Token::Hash => (Sym::Xor, 6),
21022            Token::Adjacent => (Sym::RangeAdjacent, 5),
21023            _ => return Ok(None),
21024        };
21025        if prec < min_prec {
21026            return Ok(None);
21027        }
21028        self.advance();
21029        let rhs = self.parse_expr(prec + 1)?;
21030        let out = match sym {
21031            Sym::Regex { ci, negated } => {
21032                let mut args = alloc::vec![lhs.clone(), rhs];
21033                if ci {
21034                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21035                }
21036                maybe_not(
21037                    Expr::FunctionCall {
21038                        name: String::from("regexp_like"),
21039                        args,
21040                    },
21041                    negated,
21042                )
21043            }
21044            Sym::Like { ci, negated } => Expr::Like {
21045                expr: alloc::boxed::Box::new(lhs.clone()),
21046                pattern: alloc::boxed::Box::new(rhs),
21047                negated,
21048                case_insensitive: ci,
21049            },
21050            Sym::StartsWith => Expr::FunctionCall {
21051                name: String::from("starts_with"),
21052                args: alloc::vec![lhs.clone(), rhs],
21053            },
21054            Sym::Power => Expr::FunctionCall {
21055                name: String::from("power"),
21056                args: alloc::vec![lhs.clone(), rhs],
21057            },
21058            // `#` bitwise XOR — a real operator now (was desugared to
21059            // `(a|b)-(a&b)`, algebraically identical for integers but
21060            // undefined for bit strings; the direct op handles both).
21061            Sym::Xor => Expr::Binary {
21062                lhs: Box::new(lhs.clone()),
21063                op: BinOp::BitXor,
21064                rhs: Box::new(rhs),
21065            },
21066            // range `-|-` "is adjacent to" — lowered to a catalog function.
21067            Sym::RangeAdjacent => Expr::FunctionCall {
21068                name: String::from("range_adjacent"),
21069                args: alloc::vec![lhs.clone(), rhs],
21070            },
21071        };
21072        Ok(Some(out))
21073    }
21074
21075    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21076    /// predicates, moved out of the tight postfix-cast loop: PG binds
21077    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21078    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21079    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21080    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21081    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21082    /// when nothing at this position belongs to the family. Out-of-line
21083    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21084    /// chain that MAX_NEST_DEPTH is tuned against.
21085    #[inline(never)]
21086    fn parse_postfix_predicate(
21087        &mut self,
21088        lhs: &Expr,
21089        min_prec: u8,
21090    ) -> Result<Option<Expr>, ParseError> {
21091        // Reached through try_symbol_operator (an existing leaf call of
21092        // the binary loop) so NO new stack slots land on the per-nesting
21093        // frame chain; the lhs clones only when a predicate actually
21094        // consumes it.
21095        match self.peek() {
21096            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21097            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21098            Token::Is if min_prec <= 4 => {}
21099            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21100            Token::Not
21101                if min_prec <= 5
21102                    && matches!(
21103                        self.tokens.get(self.pos + 1),
21104                        Some(Token::Between | Token::In | Token::Like)
21105                    ) => {}
21106            Token::Not | Token::Ident(_)
21107                if min_prec <= 5
21108                    && (matches!(self.peek(), Token::Ident(s)
21109                            if s.eq_ignore_ascii_case("ilike")
21110                                || (self.mysql_dialect
21111                                    && (s.eq_ignore_ascii_case("regexp")
21112                                        || s.eq_ignore_ascii_case("rlike")))
21113                                || (s.eq_ignore_ascii_case("similar")
21114                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21115                        || (matches!(self.peek(), Token::Not)
21116                            && matches!(self.tokens.get(self.pos + 1),
21117                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21118                                    || (self.mysql_dialect
21119                                        && (s.eq_ignore_ascii_case("regexp")
21120                                            || s.eq_ignore_ascii_case("rlike")))
21121                                    || s.eq_ignore_ascii_case("similar")))) => {}
21122            _ => return Ok(None),
21123        }
21124        let mut expr = lhs.clone();
21125        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21126        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21127        if min_prec <= 4 {
21128            if matches!(self.peek(), Token::Is) {
21129                self.advance();
21130                let negated = if matches!(self.peek(), Token::Not) {
21131                    self.advance();
21132                    true
21133                } else {
21134                    false
21135                };
21136                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21137                // mailrs pg_dump.
21138                if matches!(self.peek(), Token::Distinct) {
21139                    self.advance();
21140                    if !matches!(self.peek(), Token::From) {
21141                        return Err(self.err(format!(
21142                            "expected FROM after IS{} DISTINCT, got {:?}",
21143                            if negated { " NOT" } else { "" },
21144                            self.peek()
21145                        )));
21146                    }
21147                    self.advance();
21148                    // Right-hand side: parse at the same precedence
21149                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21150                    // groups as `x IS DISTINCT FROM (a + b)`.
21151                    let rhs = self.parse_expr(5)?;
21152                    let op = if negated {
21153                        BinOp::IsNotDistinctFrom
21154                    } else {
21155                        BinOp::IsDistinctFrom
21156                    };
21157                    expr = Expr::Binary {
21158                        op,
21159                        lhs: Box::new(expr),
21160                        rhs: Box::new(rhs),
21161                    };
21162                    {
21163                        return Ok(Some(expr));
21164                    }
21165                }
21166                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21167                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21168                // Lowers onto pg_is_json(x, kind); NOT wraps the
21169                // call in a logical negation.
21170                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21171                if s.eq_ignore_ascii_case("json"))
21172                {
21173                    self.advance(); // JSON
21174                    let kind = match self.peek() {
21175                        Token::Ident(s) | Token::QuotedIdent(s)
21176                            if matches!(
21177                                s.to_ascii_lowercase().as_str(),
21178                                "value" | "object" | "array" | "scalar"
21179                            ) =>
21180                        {
21181                            let k = s.to_ascii_lowercase();
21182                            self.advance();
21183                            k
21184                        }
21185                        _ => "value".to_string(),
21186                    };
21187                    let call = Expr::FunctionCall {
21188                        name: "pg_is_json".to_string(),
21189                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21190                    };
21191                    expr = if negated {
21192                        Expr::Unary {
21193                            op: UnOp::Not,
21194                            expr: Box::new(call),
21195                        }
21196                    } else {
21197                        call
21198                    };
21199                    {
21200                        return Ok(Some(expr));
21201                    }
21202                }
21203                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21204                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21205                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21206                {
21207                    let form_kw = match self.peek() {
21208                        Token::Ident(s) | Token::QuotedIdent(s)
21209                            if matches!(
21210                                s.to_ascii_uppercase().as_str(),
21211                                "NFC" | "NFD" | "NFKC" | "NFKD"
21212                            ) && matches!(
21213                                self.tokens.get(self.pos + 1),
21214                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21215                                    if n.eq_ignore_ascii_case("normalized")
21216                            ) =>
21217                        {
21218                            Some(s.to_ascii_uppercase())
21219                        }
21220                        _ => None,
21221                    };
21222                    let bare_normalized = form_kw.is_none()
21223                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21224                        if s.eq_ignore_ascii_case("normalized"));
21225                    if form_kw.is_some() || bare_normalized {
21226                        if form_kw.is_some() {
21227                            self.advance(); // form keyword
21228                        }
21229                        self.advance(); // NORMALIZED
21230                        let mut args = alloc::vec![expr];
21231                        if let Some(f) = form_kw {
21232                            args.push(Expr::Literal(Literal::String(f)));
21233                        }
21234                        let call = Expr::FunctionCall {
21235                            name: "is_normalized".to_string(),
21236                            args,
21237                        };
21238                        expr = if negated {
21239                            Expr::Unary {
21240                                op: UnOp::Not,
21241                                expr: Box::new(call),
21242                            }
21243                        } else {
21244                            call
21245                        };
21246                        {
21247                            return Ok(Some(expr));
21248                        }
21249                    }
21250                }
21251                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21252                // three-valued boolean tests. IS TRUE/FALSE never
21253                // return NULL, so they lower to CASE forms whose
21254                // ELSE catches the NULL branch; IS UNKNOWN on a
21255                // boolean is exactly IS NULL.
21256                if matches!(self.peek(), Token::True | Token::False)
21257                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21258                {
21259                    let tok = self.advance();
21260                    let test = match tok {
21261                        Token::True => Some(true),
21262                        Token::False => Some(false),
21263                        _ => None, // UNKNOWN
21264                    };
21265                    // v7.39 (round 328, V45) — kept as what the user
21266                    // wrote. These used to be lowered here into `CASE` /
21267                    // `IS NULL`; the semantics were right but the AST no
21268                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21269                    // was echoed back as
21270                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21271                    expr = Expr::BoolTest {
21272                        expr: Box::new(expr),
21273                        value: test,
21274                        negated,
21275                    };
21276                    {
21277                        return Ok(Some(expr));
21278                    }
21279                }
21280                if !matches!(self.peek(), Token::Null) {
21281                    return Err(self.err(format!(
21282                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21283                    if negated { " NOT" } else { "" },
21284                    self.peek()
21285                )));
21286                }
21287                self.advance();
21288                expr = Expr::IsNull {
21289                    expr: Box::new(expr),
21290                    negated,
21291                };
21292                {
21293                    return Ok(Some(expr));
21294                }
21295            }
21296        }
21297        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21298        if min_prec <= 5 {
21299            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21300            // Look one token ahead so a stray `NOT` not followed by any of
21301            // these flows through to the early return below untouched.
21302            let negated = if matches!(self.peek(), Token::Not) {
21303                let next = self.tokens.get(self.pos + 1);
21304                matches!(next, Some(Token::Between | Token::In | Token::Like))
21305                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21306                    || (self.mysql_dialect
21307                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21308                    || s.eq_ignore_ascii_case("similar"))
21309            } else {
21310                false
21311            };
21312            if negated {
21313                self.advance();
21314            }
21315            if matches!(self.peek(), Token::Between) {
21316                expr = self.parse_between_tail(expr, negated)?;
21317                {
21318                    return Ok(Some(expr));
21319                }
21320            }
21321            if matches!(self.peek(), Token::In) {
21322                if self.suppress_in_tail && !negated {
21323                    // POSITION(sub IN str) — IN belongs to the
21324                    // enclosing function syntax; stop here.
21325                    {
21326                        return Ok(None);
21327                    }
21328                }
21329                expr = self.parse_in_tail(expr, negated)?;
21330                {
21331                    return Ok(Some(expr));
21332                }
21333            }
21334            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21335            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21336            // (the SQL→regex transform runs inside, in the backtracking-
21337            // friendly shape SPG's matcher needs).
21338            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21339                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21340            {
21341                self.advance(); // SIMILAR
21342                self.advance(); // TO
21343                let pattern = self.parse_expr(6)?;
21344                let mut args = alloc::vec![expr, pattern];
21345                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21346                    self.advance();
21347                    args.push(self.parse_expr(6)?);
21348                }
21349                let call = Expr::FunctionCall {
21350                    name: "__similar_to".to_string(),
21351                    args,
21352                };
21353                expr = maybe_not(call, negated);
21354                {
21355                    return Ok(Some(expr));
21356                }
21357            }
21358            if matches!(self.peek(), Token::Like) {
21359                self.advance();
21360                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21361                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21362                    expr = q;
21363                    {
21364                        return Ok(Some(expr));
21365                    }
21366                }
21367                // Pattern at the same precedence as other comparison RHSes —
21368                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21369                let mut pattern = self.parse_expr(6)?;
21370                // `ESCAPE 'c'` — rewrite a literal pattern to the
21371                // default backslash escape at parse time. Custom
21372                // escapes on non-literal patterns would need
21373                // matcher support; error honestly.
21374                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21375                    self.advance();
21376                    let esc = self.parse_expr(6)?;
21377                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21378                }
21379                expr = Expr::Like {
21380                    expr: Box::new(expr),
21381                    pattern: Box::new(pattern),
21382                    negated,
21383                    case_insensitive: false,
21384                };
21385                {
21386                    return Ok(Some(expr));
21387                }
21388            }
21389            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21390            // keyword reaches us as a plain identifier.
21391            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21392                self.advance();
21393                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21394                    expr = q;
21395                    {
21396                        return Ok(Some(expr));
21397                    }
21398                }
21399                let pattern = self.parse_expr(6)?;
21400                expr = Expr::Like {
21401                    expr: Box::new(expr),
21402                    pattern: Box::new(pattern),
21403                    negated,
21404                    case_insensitive: true,
21405                };
21406                {
21407                    return Ok(Some(expr));
21408                }
21409            }
21410            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21411            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21412            // matches case-insensitively under the default collation, so it
21413            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21414            // `~*` operator uses, wrapped in NOT when negated.
21415            if self.mysql_dialect
21416                && matches!(self.peek(), Token::Ident(s)
21417                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21418            {
21419                self.advance();
21420                let pattern = self.parse_expr(6)?;
21421                let call = Expr::FunctionCall {
21422                    name: String::from("regexp_like"),
21423                    args: alloc::vec![
21424                        expr,
21425                        pattern,
21426                        Expr::Literal(Literal::String(String::from("i"))),
21427                    ],
21428                };
21429                return Ok(Some(maybe_not(call, negated)));
21430            }
21431        }
21432        let _ = expr;
21433        Ok(None)
21434    }
21435
21436    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21437        let mut lhs = self.parse_unary()?;
21438        let mut chain_len = 0usize;
21439        loop {
21440            // OPERATOR([schema.]op) reduces to its underlying
21441            // operator token before the normal dispatch.
21442            let explicit = self.peek_explicit_operator();
21443            let dispatch = match &explicit {
21444                Some((_, tok)) => self.binop_here(tok),
21445                None => self.binop_here(self.peek()),
21446            };
21447            let Some((op, prec)) = dispatch else {
21448                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21449                // of the symbol family. `binop_here` answers None for them
21450                // because they lower onto function calls rather than a
21451                // BinOp, and the fallback below reads `self.peek()` — the
21452                // word OPERATOR, not the operator. `pg_dump` writes every
21453                // catalog predicate this way, so its first query failed
21454                // and no dump ran:
21455                //
21456                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21457                //
21458                // Collapsing the wrapper to the operator it names puts the
21459                // token where the fallback already looks.
21460                if let Some((next, op_tok)) = explicit {
21461                    self.tokens.splice(self.pos..next, [op_tok]);
21462                }
21463                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21464                    lhs = e;
21465                    chain_len += 1;
21466                    if chain_len > MAX_BINARY_CHAIN {
21467                        return Err(self.err(alloc::format!(
21468                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21469                        )));
21470                    }
21471                    continue;
21472                }
21473                break;
21474            };
21475            if prec < min_prec {
21476                break;
21477            }
21478            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21479            // iteratively but evaluates and drops recursively;
21480            // depth beyond the budget overflows worker stacks.
21481            chain_len += 1;
21482            if chain_len > MAX_BINARY_CHAIN {
21483                return Err(self.err(alloc::format!(
21484                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21485                )));
21486            }
21487            match explicit {
21488                Some((end_pos, _)) => self.pos = end_pos,
21489                None => {
21490                    self.advance();
21491                }
21492            }
21493            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21494            // ANY is a bare ident; ALL is a reserved Token. Both
21495            // require an immediate `(` to disambiguate from
21496            // identifier columns named `any` / `all`.
21497            let any_kind = match self.peek() {
21498                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21499                    Some(false)
21500                }
21501                Token::Ident(s) | Token::QuotedIdent(s)
21502                    if (s.eq_ignore_ascii_case("any")
21503                        || s.eq_ignore_ascii_case("some")
21504                        || s.eq_ignore_ascii_case("all"))
21505                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21506                {
21507                    Some(!s.eq_ignore_ascii_case("all"))
21508                }
21509                _ => None,
21510            };
21511            if let Some(is_any) = any_kind {
21512                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21513                continue;
21514            }
21515            let rhs = self.parse_expr(prec + 1)?;
21516            lhs = Expr::Binary {
21517                lhs: Box::new(lhs),
21518                op,
21519                rhs: Box::new(rhs),
21520            };
21521        }
21522        Ok(lhs)
21523    }
21524
21525    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21526    /// and the array form.
21527    ///
21528    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21529    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21530    /// this block's `Expr` temporaries and four `format!` sites slots in
21531    /// that frame on every level of `((((1))))`, which never reaches it.
21532    #[inline(never)]
21533    fn parse_any_all_rhs(
21534        &mut self,
21535        lhs: Expr,
21536        op: BinOp,
21537        is_any: bool,
21538    ) -> Result<Expr, ParseError> {
21539        self.advance(); // ident
21540        self.advance(); // (
21541        // `x op ANY (SELECT …)` — the quantified-subquery
21542        // form. `= ANY` is exactly IN; the other operators
21543        // lower onto EXISTS over the subquery as a derived
21544        // table, comparing against its single projection
21545        // aliased __v (x's columns resolve correlated).
21546        // ALL is the negated-EXISTS complement; a NULL
21547        // element makes PG return NULL where this lowering
21548        // returns true — the NOT NULL column case (the
21549        // practical one) is exact.
21550        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21551            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21552            // legal PG too (round-151 sibling). Out-of-line
21553            // (#[inline(never)] helper) — this sits on
21554            // parse_expr's recursive frame and the two-armed
21555            // SELECT temporary blew the nesting-budget stack.
21556            let mut sub = self.parse_any_all_select_body()?;
21557            if !matches!(self.peek(), Token::RParen) {
21558                return Err(self.err(alloc::format!(
21559                    "expected ')' after ANY/ALL subquery, got {:?}",
21560                    self.peek()
21561                )));
21562            }
21563            self.advance();
21564            if sub.items.len() != 1 {
21565                return Err(self.err(alloc::format!(
21566                    "ANY/ALL subquery must return one column, got {}",
21567                    sub.items.len()
21568                )));
21569            }
21570            if is_any && matches!(op, BinOp::Eq) {
21571                return Ok(Expr::InSubquery {
21572                    expr: Box::new(lhs),
21573                    subquery: Box::new(sub),
21574                    negated: false,
21575                });
21576            }
21577            // The engine's subquery resolvers materialise
21578            // the single-column result into an ARRAY the
21579            // existing AnyAll three-valued eval consumes.
21580            return Ok(Expr::AnyAll {
21581                expr: Box::new(lhs),
21582                op,
21583                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21584                is_any,
21585            });
21586        }
21587        let arr = self.parse_expr(0)?;
21588        if !matches!(self.peek(), Token::RParen) {
21589            return Err(self.err(alloc::format!(
21590                "expected ')' after ANY/ALL argument, got {:?}",
21591                self.peek()
21592            )));
21593        }
21594        self.advance();
21595        Ok(Expr::AnyAll {
21596            expr: Box::new(lhs),
21597            op,
21598            array: Box::new(arr),
21599            is_any,
21600        })
21601    }
21602
21603    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21604    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21605    #[inline(never)]
21606    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21607        self.advance();
21608        let e = self.parse_expr(9)?;
21609        Ok(build_center_call(e))
21610    }
21611
21612    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21613    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21614    /// unary minus.
21615    ///
21616    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21617    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21618    /// the Expr-sized local stays out of that frame.
21619    #[inline(never)]
21620    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21621        self.advance();
21622        let e = self.parse_expr(9)?;
21623        Ok(Expr::FunctionCall {
21624            name: alloc::string::String::from(name),
21625            args: alloc::vec![e],
21626        })
21627    }
21628
21629    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21630    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21631    #[inline(never)]
21632    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21633        self.advance();
21634        let e = self.parse_expr(9)?;
21635        Ok(Expr::FunctionCall {
21636            name: alloc::string::String::from(if vertical {
21637                "isvertical"
21638            } else {
21639                "ishorizontal"
21640            }),
21641            args: alloc::vec![e],
21642        })
21643    }
21644
21645    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21646    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21647    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21648    #[inline(never)]
21649    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21650        self.advance();
21651        let e = self.parse_expr(9)?;
21652        Ok(Expr::Cast {
21653            expr: Box::new(e),
21654            target: CastTarget::Named("binary".to_string()),
21655        })
21656    }
21657
21658    /// The prefix operators that share one shape: take the token, parse
21659    /// an operand at `prec`, wrap it.
21660    ///
21661    /// `#[inline(never)]`, and one function instead of five arms, for the
21662    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21663    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21664    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21665    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21666    /// five `Expr`-sized locals per level for them anyway.
21667    #[inline(never)]
21668    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21669        self.advance();
21670        let e = self.parse_expr(prec)?;
21671        Ok(Expr::Unary {
21672            op,
21673            expr: Box::new(e),
21674        })
21675    }
21676
21677    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21678    /// and separate from it because of the literal folding below and the
21679    /// `format!` temporaries that folding needs.
21680    #[inline(never)]
21681    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21682        self.advance();
21683        // v7.39 (round 549) — fold the sign into an integer literal that
21684        // only fits once it is negative.
21685        //
21686        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21687        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21688        // folds the sign first, so `-9223372036854775808` is a bigint
21689        // there — and `-9223372036854775808 - 1` raises "bigint out of
21690        // range" where SPG quietly answered -9223372036854775809, a value
21691        // no bigint can hold. The arithmetic itself was already checked;
21692        // only the literal's type was wrong.
21693        if let Token::Numeric(lit) = self.peek()
21694            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21695        {
21696            self.advance();
21697            return Ok(Expr::Literal(Literal::Integer(folded)));
21698        }
21699        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21700        // `<->` slotted into 5 and arithmetic shifted up).
21701        let e = self.parse_expr(9)?;
21702        Ok(Expr::Unary {
21703            op: UnOp::Neg,
21704            expr: Box::new(e),
21705        })
21706    }
21707
21708    /// tsquery `!!` prefix negation, lowered to the catalog function.
21709    /// Binds like unary minus. Out-of-line for the frame reason on
21710    /// `parse_unary_op`.
21711    #[inline(never)]
21712    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21713        self.advance();
21714        let e = self.parse_expr(9)?;
21715        Ok(Expr::FunctionCall {
21716            name: String::from("tsquery_not"),
21717            args: alloc::vec![e],
21718        })
21719    }
21720
21721    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21722        match self.peek() {
21723            // NOT binds tighter than AND / XOR / OR but looser than
21724            // comparisons — its operand takes everything ≥ the comparison
21725            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21726            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21727            // was rung 3, behaviour-identical when 3 was unused; AND now
21728            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21729            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21730            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21731            // The body is out-of-line: `parse_unary` is one of the three
21732            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21733            // inline arm here overflowed the native stack in
21734            // `nesting_budget_errors_cleanly` — the guard test caught it,
21735            // exactly as the eval-side cliff did in rounds 346 and 351.
21736            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21737                self.parse_binary_prefix()
21738            }
21739            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21740            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21741            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21742            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21743            Token::Minus => self.parse_prefix_minus(),
21744            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21745            // worked only because the lexer reads it as one signed literal;
21746            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21747            // PG18 and MariaDB take all of them. Binds like unary minus.
21748            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21749            // Bitwise NOT binds like unary minus.
21750            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21751            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21752            // "center of" operator; desugars to center(x). The whole arm
21753            // is out-of-line: parse_unary sits on the per-nesting-level
21754            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21755            // Expr-sized local may live in this frame.
21756            Token::TsMatch => self.parse_prefix_center(),
21757            // v7.39 (round 508) — the prefix operators that are named
21758            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21759            // is length. Out-of-line for the same nesting-frame reason as
21760            // parse_prefix_center — parse_unary sits on the recursive cycle
21761            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21762            // live in this frame.
21763            Token::At => self.parse_prefix_call("abs"),
21764            Token::Hash => self.parse_prefix_call("npoints"),
21765            Token::AtMinusAt => self.parse_prefix_call("length"),
21766            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21767            // "is horizontal" (lseg / line); desugars to the existing
21768            // isvertical()/ishorizontal() functions. Out-of-line for the
21769            // same nesting-frame reason as parse_prefix_center.
21770            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21771            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21772            Token::DoubleBang => self.parse_prefix_tsquery_not(),
21773            _ => self.parse_atom(),
21774        }
21775    }
21776
21777    /// Parse a parenthesised scalar subquery body after the caller has consumed
21778    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21779    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21780    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21781    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21782    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21783    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21784    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21785    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21786    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21787    /// tips the deep-nesting test into a stack overflow).
21788    #[inline(never)]
21789    fn array_subquery_ahead(&self) -> bool {
21790        if !matches!(self.peek(), Token::LParen) {
21791            return false;
21792        }
21793        matches!(
21794            self.tokens.get(self.pos + 1),
21795            Some(Token::Select | Token::Values)
21796        ) || matches!(
21797            self.tokens.get(self.pos + 1),
21798            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21799        )
21800    }
21801
21802    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21803    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21804    /// locals stay off parse_atom's recursive frame (round 105).
21805    #[inline(never)]
21806    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21807        self.advance(); // consume `[`
21808        let mut items: Vec<Expr> = Vec::new();
21809        if !matches!(self.peek(), Token::RBracket) {
21810            loop {
21811                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21812                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21813                if matches!(self.peek(), Token::LBracket) {
21814                    items.push(self.parse_array_bracket_body()?);
21815                } else {
21816                    items.push(self.parse_expr(0)?);
21817                }
21818                match self.peek() {
21819                    Token::Comma => {
21820                        self.advance();
21821                    }
21822                    Token::RBracket => break,
21823                    other => {
21824                        return Err(self.err(alloc::format!(
21825                            "expected ',' or ']' in ARRAY literal, got {other:?}"
21826                        )));
21827                    }
21828                }
21829            }
21830        }
21831        self.advance(); // consume `]`
21832        Ok(Expr::Array(items))
21833    }
21834
21835    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21836    /// is already consumed; the current token is `(`. Desugars to a scalar
21837    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21838    /// the subquery's single-column rows in order — reusing the existing
21839    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21840    /// keeps the large `Statement` local off parse_atom's recursive frame.
21841    #[inline(never)]
21842    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21843        self.advance(); // consume `(`
21844        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21845            if w.eq_ignore_ascii_case("with"));
21846        let sub = if is_with {
21847            self.advance(); // WITH
21848            self.parse_with_cte_then_select()?
21849        } else {
21850            self.parse_select_stmt()?
21851        };
21852        if !matches!(self.peek(), Token::RParen) {
21853            return Err(self.err(alloc::format!(
21854                "expected ')' to close ARRAY(subquery), got {:?}",
21855                self.peek()
21856            )));
21857        }
21858        self.advance(); // consume `)`
21859        // Reuse the parser to build the array_agg wrapper from the subquery's
21860        // canonical text — avoids hand-constructing the derived-table AST.
21861        let wrapper = alloc::format!(
21862            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21863        );
21864        let stmt = parse_statement(&wrapper)
21865            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21866        let Statement::Select(sel) = stmt else {
21867            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21868        };
21869        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21870    }
21871
21872    #[inline(never)]
21873    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21874        let inner = if is_with {
21875            self.advance(); // WITH
21876            self.parse_with_cte_then_select()?
21877        } else {
21878            self.parse_select_stmt()?
21879        };
21880        match self.advance() {
21881            Token::RParen => {
21882                let Statement::Select(s) = inner else {
21883                    return Err(ParseError {
21884                        message: "scalar subquery body must be a SELECT".into(),
21885                        token_pos: self.consumed_pos(),
21886                    });
21887                };
21888                Ok(Expr::ScalarSubquery(Box::new(s)))
21889            }
21890            other => Err(ParseError {
21891                message: format!("expected ')' after scalar subquery, got {other:?}"),
21892                token_pos: self.consumed_pos(),
21893            }),
21894        }
21895    }
21896
21897    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21898    /// literals. The lexer splits them into an ident + string; recombine
21899    /// here. Out-of-line and returning `Option` so `parse_atom` — the
21900    /// recursive frame the 768 KiB stack budget is tuned against — pays no
21901    /// frame for the `body` / `bits` strings and their char loops (the
21902    /// round-367 frame cliff, M20).
21903    #[inline(never)]
21904    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21905        let is_hex = match self.peek() {
21906            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21907            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21908            _ => return None,
21909        };
21910        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21911            return None;
21912        }
21913        self.advance();
21914        let Token::String(body) = self.advance() else {
21915            unreachable!("guarded above");
21916        };
21917        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21918        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21919        // (hex pairs, even count required — MariaDB errors on an odd
21920        // count); `b'1010'` packs its bits big-endian, left-padded to a
21921        // byte. Lower both onto the bytea cast.
21922        if self.mysql_dialect {
21923            if is_hex {
21924                if body.len() % 2 == 1 {
21925                    return Some(Err(self.err(alloc::format!(
21926                        "invalid hex string literal X'{body}': odd digit count"
21927                    ))));
21928                }
21929                for c in body.chars() {
21930                    if !c.is_ascii_hexdigit() {
21931                        return Some(Err(
21932                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21933                        ));
21934                    }
21935                }
21936                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21937            }
21938            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21939                return Some(Err(
21940                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21941                ));
21942            }
21943            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21944        }
21945        let bits = if is_hex {
21946            let mut out = String::with_capacity(body.len() * 4);
21947            for c in body.chars() {
21948                let Some(d) = c.to_digit(16) else {
21949                    return Some(Err(self.err(alloc::format!(
21950                        "invalid hexadecimal digit {c:?} in X'…' bit string"
21951                    ))));
21952                };
21953                out.push_str(&alloc::format!("{d:04b}"));
21954            }
21955            out
21956        } else {
21957            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21958                return Some(Err(self.err(alloc::format!(
21959                    "invalid binary digit {bad:?} in B'…' bit string"
21960                ))));
21961            }
21962            body
21963        };
21964        // Route through the postfix-cast loop so a chained cast like
21965        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21966        // of erroring at the `::`.
21967        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21968        // literal keeps its exact length, while an explicit `::bit` cast is
21969        // bit(1) with pad/truncate semantics (PG).
21970        Some(self.finish_postfix_casts(Expr::Cast {
21971            expr: Box::new(Expr::Literal(Literal::String(bits))),
21972            target: CastTarget::Named("__bit_literal".to_string()),
21973        }))
21974    }
21975
21976    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21977        if let Some(res) = self.try_parse_bit_string_literal() {
21978            return res;
21979        }
21980        let tok_pos = self.pos;
21981        match self.advance() {
21982            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21983            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21984            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21985            // carrying the source mantissa + scale so no precision is lost. A
21986            // literal too wide for i128 falls back to double precision.
21987            // Out-of-line (#[inline(never)]) — this arm sits on the
21988            // parse_expr recursion chain; its expansion locals must not
21989            // widen the recursive frame (debug frame-cliff discipline).
21990            Token::Numeric(s) => match numeric_token_to_literal(s) {
21991                Ok(lit) => Ok(Expr::Literal(lit)),
21992                Err(msg) => Err(self.err(msg)),
21993            },
21994            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21995            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21996            // (the lexer only emits this token in the MySQL dialect). Lower
21997            // onto the existing bytea cast; out-of-line to keep this arm off
21998            // the parse recursion frame.
21999            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22000            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22001            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22002            Token::Null => Ok(Expr::Literal(Literal::Null)),
22003            // v6.1.1 — `$N` placeholder. The actual Value lookup
22004            // happens in the engine eval path against the prepared-
22005            // statement bind buffer.
22006            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22007            Token::LParen => {
22008                // v4.10: `(SELECT ...)` in expression position is a
22009                // scalar subquery; otherwise it's a parenthesised
22010                // expression. Peek for SELECT keyword to dispatch.
22011                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22012                // lexes as Ident("with") (not a reserved token). The subquery body
22013                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22014                // so its large `Statement` local stays out of parse_atom's stack
22015                // frame — parse_atom is on the recursive `((…))` cycle and the
22016                // nesting budget is tuned to its frame size).
22017                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22018                    if s.eq_ignore_ascii_case("with"));
22019                if matches!(self.peek(), Token::Select) || is_with {
22020                    self.parse_paren_scalar_subquery(is_with)
22021                } else {
22022                    let e = self.parse_expr(0)?;
22023                    // `(a, b, …)` — a row constructor. Valid only
22024                    // in front of a comparison operator or [NOT]
22025                    // IN; both expand at parse time (lexicographic
22026                    // comparison / OR'd row equalities).
22027                    if matches!(self.peek(), Token::Comma) {
22028                        let mut row = alloc::vec![e];
22029                        while matches!(self.peek(), Token::Comma) {
22030                            self.advance();
22031                            row.push(self.parse_expr(0)?);
22032                        }
22033                        if !matches!(self.peek(), Token::RParen) {
22034                            return Err(self.err(alloc::format!(
22035                                "expected ')' after row constructor, got {:?}",
22036                                self.peek()
22037                            )));
22038                        }
22039                        self.advance();
22040                        // A bare `(a, b, …)` row constructor can carry postfix
22041                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22042                        // early return here skips parse_atom's tail postfix
22043                        // pass, so fold casts in explicitly. For the
22044                        // comparison / predicate forms nothing postfix follows,
22045                        // so this is a no-op.
22046                        return self
22047                            .parse_row_comparison_tail(row)
22048                            .and_then(|e| self.finish_postfix_casts(e));
22049                    }
22050                    match self.advance() {
22051                        Token::RParen => Ok(e),
22052                        other => Err(ParseError {
22053                            message: format!("expected ')', got {other:?}"),
22054                            token_pos: self.consumed_pos(),
22055                        }),
22056                    }
22057                }
22058            }
22059            Token::LBracket => self.parse_vector_literal_body(),
22060            Token::Extract => self.parse_extract_atom(),
22061            Token::Interval => self.parse_interval_atom(),
22062            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22063            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22064            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22065            // expression position calling the PG `left(string, n)` /
22066            // `right(string, n)` function; rebuild the AST as a regular
22067            // function call so the engine's apply_function dispatch picks
22068            // it up. Delegated to a #[inline(never)] helper so its locals
22069            // don't bloat this recursive `parse_atom` frame (the nesting
22070            // budget in `enter_nested` is tuned to parse_atom's size).
22071            Token::Left if matches!(self.peek(), Token::LParen) => {
22072                self.parse_lr_string_function_call("left")
22073            }
22074            Token::Right if matches!(self.peek(), Token::LParen) => {
22075                self.parse_lr_string_function_call("right")
22076            }
22077            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22078            // token; we match on the bare ident. NOT is a token
22079            // (consumed in the comparison rung), but `EXISTS (...)`
22080            // at the top of an expression starts here.
22081            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22082                self.parse_exists_atom(false)
22083            }
22084            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22085            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22086            // CASE is a bare ident; we dispatch on lowercase match.
22087            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22088                self.parse_case_atom()
22089            }
22090            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22091            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22092            // '…'`. Lower onto the ::cast node so the existing
22093            // runtime text→date/timestamp paths do the parsing. The
22094            // string must follow immediately, else the ident stays a
22095            // plain column reference.
22096            Token::Ident(s)
22097                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22098                    && matches!(self.peek(), Token::String(_)) =>
22099            {
22100                let target =
22101                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22102                let Token::String(lit) = self.advance() else {
22103                    unreachable!("peek guaranteed a string token");
22104                };
22105                Ok(Expr::Cast {
22106                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22107                    target,
22108                })
22109            }
22110            // v7.39 (round 221) — the SQL-standard long spellings:
22111            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22112            // TIME ZONE '…'`. Consume the modifier and lower to the same
22113            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22114            Token::Ident(s)
22115                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22116                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22117                        || w.eq_ignore_ascii_case("without"))
22118                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22119                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22120                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22121            {
22122                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22123                self.advance(); // WITH / WITHOUT
22124                self.advance(); // TIME
22125                self.advance(); // ZONE
22126                let Token::String(lit) = self.advance() else {
22127                    unreachable!("guard checked a string token");
22128                };
22129                let base = s.to_ascii_lowercase();
22130                let target = match (base.as_str(), with_tz) {
22131                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22132                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22133                    (_, true) => CastTarget::Timestamptz,
22134                    (_, false) => CastTarget::Timestamp,
22135                };
22136                Ok(Expr::Cast {
22137                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22138                    target,
22139                })
22140            }
22141            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22142            // gathers the subquery's single-column rows (in its row order)
22143            // into an array. Desugared to `array_agg` over the subquery as a
22144            // derived table; out-of-line to keep parse_atom's frame small (it
22145            // sits on the recursive nesting-budget cycle).
22146            Token::Ident(s) | Token::QuotedIdent(s)
22147                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22148            {
22149                self.parse_array_subquery()
22150            }
22151            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22152            // is not a reserved token; we match by case-insensitive
22153            // ident. The opening `[` must follow immediately. v7.39 (read01
22154            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22155            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22156            Token::Ident(s) | Token::QuotedIdent(s)
22157                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22158            {
22159                self.parse_array_literal_body()
22160            }
22161            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22162            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22163            // We special-case before the generic ident dispatch so
22164            // the AGAINST clause never reaches the function-call
22165            // loop (which would mis-read `(cols) AGAINST` as a
22166            // call with no trailing modifier). The shape is
22167            // rewritten to a Boolean OR over per-column
22168            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22169            // term)` so the existing FTS evaluator handles
22170            // semantics — the fulltext-GIN built at CREATE TABLE
22171            // time is currently a "real index that survives dump
22172            // round-trip"; the planner hook that actually uses
22173            // it for posting-list intersection lands in a later
22174            // sub-phase (Phase 2.2b) without touching this surface.
22175            Token::Ident(s) | Token::QuotedIdent(s)
22176                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22177            {
22178                self.parse_match_against_atom()
22179            }
22180            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22181            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22182            // alias names in expression context too. `release` appears
22183            // in sentori `0003_partition_events.sql` as both a column
22184            // reference (SELECT … release …) and an INSERT column list
22185            // entry. Mirrors `expect_ident_like`'s expansion of the
22186            // identifier set.
22187            other if unreserved_keyword_text(&other).is_some() => {
22188                let s = unreserved_keyword_text(&other).unwrap();
22189                self.finish_ident_atom(s)
22190            }
22191            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22192            // only inside `SET` before, so `SELECT @@autocommit` — which
22193            // every MySQL connector asks at handshake — was a parse error.
22194            // MariaDB accepts the bare, `@@session.` and `@@global.`
22195            // spellings alike and answers from the session's own value.
22196            Token::SessionVar(v) => {
22197                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22198                // has nothing to do with a `@@` engine setting: its own
22199                // per-session namespace, and an unset one reads NULL instead
22200                // of raising. Stripping every `@` (as this did) made `@x` and
22201                // `@@x` the same node, so `SELECT @x` answered "Unknown
22202                // system variable".
22203                Ok(variable_ref_atom(&v))
22204            }
22205            other => Err(ParseError {
22206                message: format!("unexpected token {other:?} in expression"),
22207                token_pos: tok_pos,
22208            }),
22209        }
22210        // After parsing the atom, fold any postfix `::vector` casts.
22211        .and_then(|atom| self.finish_postfix_casts(atom))
22212    }
22213
22214    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22215    /// Both bind tighter than any binary op.
22216    /// Shared cast-target parser for postfix `::TYPE` and the
22217    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22218    /// If the next tokens are `( N )`, consume them and return the canonical
22219    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22220    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22221    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22222        if !matches!(self.peek(), Token::LParen) {
22223            return None;
22224        }
22225        self.advance(); // (
22226        let n = match self.advance() {
22227            Token::Integer(n) => n,
22228            _ => return Some(base.to_string()), // malformed → drop precision
22229        };
22230        if matches!(self.peek(), Token::RParen) {
22231            self.advance();
22232        }
22233        Some(alloc::format!("{base}({n})"))
22234    }
22235
22236    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22237        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22238        // schema-qualifies every cast target, and `pg_catalog.X` names
22239        // exactly the builtin type X. Consume the qualifier and let
22240        // the ordinary target parse decide.
22241        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22242            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22243        {
22244            self.advance();
22245            self.advance();
22246        }
22247        let target = match self.advance() {
22248            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22249                "int" | "integer" | "int4" => {
22250                    if matches!(self.peek(), Token::LBracket)
22251                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22252                    {
22253                        self.advance();
22254                        self.advance();
22255                        CastTarget::IntArray
22256                    } else {
22257                        CastTarget::Int
22258                    }
22259                }
22260                "bigint" | "int8" => {
22261                    if matches!(self.peek(), Token::LBracket)
22262                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22263                    {
22264                        self.advance();
22265                        self.advance();
22266                        CastTarget::BigIntArray
22267                    } else {
22268                        CastTarget::BigInt
22269                    }
22270                }
22271                "float" | "double" => CastTarget::Float,
22272                "text" => {
22273                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22274                    if matches!(self.peek(), Token::LBracket)
22275                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22276                    {
22277                        self.advance();
22278                        self.advance();
22279                        CastTarget::TextArray
22280                    } else {
22281                        CastTarget::Text
22282                    }
22283                }
22284                "bool" | "boolean" => CastTarget::Bool,
22285                "vector" => CastTarget::Vector,
22286                "date" => CastTarget::Date,
22287                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22288                // seconds precision through the Named path (the engine rounds
22289                // the sub-second field); bare `::timestamp` keeps the fast arm.
22290                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22291                    Some(named) => CastTarget::Named(named),
22292                    None => CastTarget::Timestamp,
22293                },
22294                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22295                    Some(named) => CastTarget::Named(named),
22296                    None => CastTarget::Timestamptz,
22297                },
22298                "interval" => CastTarget::Interval,
22299                "json" => CastTarget::Json,
22300                "jsonb" => CastTarget::Jsonb,
22301                // v7.39 (round 694) — these have dedicated CastTarget
22302                // variants, so they never reached the postfix `[]` handling
22303                // further down and `::regtype[]` was a SYNTAX error at the
22304                // `]`. PG has an array type for every scalar; take the
22305                // suffix here and hand the canonical `<ty>_array` name to
22306                // the engine, the same shape every other array cast uses.
22307                "regtype" if self.peek_postfix_array_brackets() => {
22308                    self.advance();
22309                    self.advance();
22310                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22311                }
22312                "regclass" if self.peek_postfix_array_brackets() => {
22313                    self.advance();
22314                    self.advance();
22315                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22316                }
22317                "regtype" => CastTarget::RegType,
22318                "regclass" => CastTarget::RegClass,
22319                // v7.12.0 — `::tsvector` / `::tsquery`.
22320                // Engine decodes the LHS text via the PG
22321                // external form parser.
22322                // v7.39 (round 352, M8) — MySQL's own cast targets.
22323                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22324                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22325                // such type, so they are taken only in that dialect and
22326                // fall through to the "type does not exist" arm otherwise.
22327                "signed" | "unsigned" if self.mysql_dialect => {
22328                    if matches!(self.peek(), Token::Ident(k)
22329                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22330                    {
22331                        self.advance();
22332                    }
22333                    CastTarget::Named(s.to_ascii_lowercase())
22334                }
22335                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22336                // in MySQL: MariaDB answers '123' where the SQL-standard
22337                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22338                // Truncating a number to its first digit is a wrong answer
22339                // with no error, so the MySQL session gets MySQL's reading.
22340                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22341                    CastTarget::Text
22342                }
22343                "tsvector" => CastTarget::TsVector,
22344                "tsquery" => CastTarget::TsQuery,
22345                // v7.17.0 — `::uuid`. Engine decodes the LHS
22346                // text via `spg_storage::parse_uuid_str`.
22347                "uuid" => CastTarget::Uuid,
22348                // v7.18 — `::bytea`. Engine decodes the LHS
22349                // text via the PG hex form (`'\xdeadbeef'`)
22350                // or escape form (`'\\x05\\x00'`). Closes
22351                // mailrs D-pre #3 reverse-acceptance gap.
22352                "bytea" => CastTarget::Bytea,
22353                // v7.37.5 ship triage — generic typed-cast escape.
22354                // Anything the long-tail PG type ident table knows
22355                // about(network/bit/geometry/multirange/etc.)flows
22356                // through `CastTarget::Named(canonical)`; the engine
22357                // resolves via `column_type_to_data_type` and dispatches
22358                // through the typed `coerce_value` path. Truly
22359                // unrecognised idents still hit the error arm below
22360                // because the engine rejects them.
22361                other => {
22362                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22363                    // `::varchar(255)`, etc. Capture into the canonical
22364                    // `name(p,s)` form so `type_name_to_data_type` can
22365                    // reconstruct the `DataType::Numeric { precision,
22366                    // scale }` (and similar param-carrying types).
22367                    let mut name = other.to_string();
22368                    // v7.39 (round 281) — `::bit varying(3)` is two
22369                    // words; fold the tail in so the typmod reaches the
22370                    // type resolver instead of tripping the parser.
22371                    if name.eq_ignore_ascii_case("bit")
22372                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22373                    {
22374                        self.advance();
22375                        name = alloc::string::String::from("varbit");
22376                    }
22377                    // v7.39 (round 613) — `::character varying` is the same
22378                    // two-word shape and had no fold, so the `varying` was
22379                    // left behind and the cast became a bare `character`,
22380                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22381                    // `a` where PG answers `ab`. Silently, and for a spelling
22382                    // pg_dump writes.
22383                    if name.eq_ignore_ascii_case("character")
22384                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22385                    {
22386                        self.advance();
22387                        name = alloc::string::String::from("varchar");
22388                    }
22389                    if matches!(self.peek(), Token::LParen) {
22390                        let mut buf = alloc::string::String::from("(");
22391                        let mut depth = 0usize;
22392                        loop {
22393                            match self.advance() {
22394                                Token::LParen => {
22395                                    depth += 1;
22396                                    if depth > 1 {
22397                                        buf.push('(');
22398                                    }
22399                                }
22400                                Token::RParen => {
22401                                    depth -= 1;
22402                                    if depth == 0 {
22403                                        buf.push(')');
22404                                        break;
22405                                    }
22406                                    buf.push(')');
22407                                }
22408                                Token::Comma => buf.push(','),
22409                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22410                                // v7.39 (round 273) — a minus used to fall
22411                                // into the catch-all below and vanish, so
22412                                // `::numeric(10,-2)` reached the engine as
22413                                // the text `numeric(10,2)` and silently
22414                                // rounded to two DECIMALS instead of to
22415                                // hundreds. A dropped token is not a
22416                                // no-op when it carries a sign.
22417                                Token::Minus => buf.push('-'),
22418                                Token::Eof => break,
22419                                _ => {}
22420                            }
22421                        }
22422                        name.push_str(&buf);
22423                    }
22424                    // Optional postfix `[]` widens to the array form —
22425                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22426                    // The engine's `type_name_to_data_type` recognises
22427                    // the canonical `<ty>_array` form.
22428                    if matches!(self.peek(), Token::LBracket)
22429                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22430                    {
22431                        self.advance();
22432                        self.advance();
22433                        name.push_str("_array");
22434                    }
22435                    CastTarget::Named(name)
22436                }
22437            },
22438            Token::Interval => CastTarget::Interval,
22439            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22440            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22441            // = char(1)); other quoted names resolve like idents.
22442            Token::QuotedIdent(q) => {
22443                if q.eq_ignore_ascii_case("char") {
22444                    CastTarget::Named("char1".into())
22445                } else {
22446                    CastTarget::Named(q.to_ascii_lowercase())
22447                }
22448            }
22449            other => {
22450                return Err(ParseError {
22451                    message: format!("expected type ident after `::`, got {other:?}"),
22452                    token_pos: self.consumed_pos(),
22453                });
22454            }
22455        };
22456        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22457        // target to its array sibling. Closed-enum arms (Bool /
22458        // SmallInt / Numeric / Float / Date / …) didn't carry the
22459        // explicit widening that Text / Int / BigInt did, so
22460        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22461        // error. The widening here mirrors the per-arm Text /
22462        // Int / BigInt logic above + folds the new ζ-A first-class
22463        // types through `CastTarget::Named("<ty>_array")`.
22464        if matches!(self.peek(), Token::LBracket)
22465            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22466        {
22467            let widened = match &target {
22468                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22469                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22470                // v7.39 (round 326, V43) — the two temporal types stay
22471                // distinct. Both used to widen to `timestamptz_array`, so
22472                // `::timestamp[]` named the wrong target in its own error
22473                // message and lost the zone-less identity on the way.
22474                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22475                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22476                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22477                CastTarget::Json | CastTarget::Jsonb => {
22478                    Some(CastTarget::Named("jsonb_array".to_string()))
22479                }
22480                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22481                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22482                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22483                CastTarget::Named(name) => {
22484                    let mut a = name.clone();
22485                    a.push_str("_array");
22486                    Some(CastTarget::Named(a))
22487                }
22488                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22489                // RegType / RegClass / TextArray / IntArray /
22490                // BigIntArray already finalised — leave as is.
22491                _ => None,
22492            };
22493            if let Some(w) = widened {
22494                self.advance();
22495                self.advance();
22496                return Ok(w);
22497            }
22498        }
22499        Ok(target)
22500    }
22501
22502    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22503        loop {
22504            // v7.38 (read01, T9) — composite field access `(expr).field`.
22505            // A bare `a.b` is consumed as a qualified column inside the ident
22506            // atom, so a Dot only survives to this postfix position when the
22507            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22508            // `.*` whole-row expansion is not handled here (projection-level).
22509            if matches!(self.peek(), Token::Dot)
22510                && matches!(
22511                    self.tokens.get(self.pos + 1),
22512                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22513                )
22514            {
22515                self.advance(); // .
22516                let field = match self.advance() {
22517                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22518                    other => {
22519                        return Err(
22520                            self.err(format!("expected a field name after '.', got {other:?}"))
22521                        );
22522                    }
22523                };
22524                expr = Expr::FieldAccess {
22525                    base: Box::new(expr),
22526                    field,
22527                };
22528                continue;
22529            }
22530            if matches!(self.peek(), Token::DoubleColon) {
22531                self.advance();
22532                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22533                // target set to include INTERVAL (reserved Token),
22534                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22535                // mailrs follow-up H3a + H3b.
22536                let target = self.parse_cast_target()?;
22537                expr = Expr::Cast {
22538                    expr: Box::new(expr),
22539                    target,
22540                };
22541                continue;
22542            }
22543            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22544            // returns NULL for out-of-range. Multiple subscripts
22545            // chain: `a[i][j]` parses left-to-right.
22546            if matches!(self.peek(), Token::LBracket) {
22547                self.advance();
22548                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22549                // bare index stays a subscript.
22550                let lo = if matches!(self.peek(), Token::Colon) {
22551                    None
22552                } else {
22553                    Some(self.parse_expr(0)?)
22554                };
22555                if matches!(self.peek(), Token::Colon) {
22556                    self.advance();
22557                    let hi = if matches!(self.peek(), Token::RBracket) {
22558                        None
22559                    } else {
22560                        Some(Box::new(self.parse_expr(0)?))
22561                    };
22562                    if !matches!(self.peek(), Token::RBracket) {
22563                        return Err(self.err(alloc::format!(
22564                            "expected ']' after array slice, got {:?}",
22565                            self.peek()
22566                        )));
22567                    }
22568                    self.advance();
22569                    expr = Expr::ArraySlice {
22570                        target: Box::new(expr),
22571                        lo: lo.map(Box::new),
22572                        hi,
22573                    };
22574                    continue;
22575                }
22576                let index = lo.expect("non-colon branch parsed an index");
22577                if !matches!(self.peek(), Token::RBracket) {
22578                    return Err(self.err(alloc::format!(
22579                        "expected ']' after array index, got {:?}",
22580                        self.peek()
22581                    )));
22582                }
22583                self.advance();
22584                expr = Expr::ArraySubscript {
22585                    target: Box::new(expr),
22586                    index: Box::new(index),
22587                };
22588                continue;
22589            }
22590            // `expr AT TIME ZONE zone` — lowers to PG's own function
22591            // form timezone(zone, expr); the scalar implements the
22592            // offset shift (named zones error there — no tzdata).
22593            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22594                && matches!(self.tokens.get(self.pos + 1),
22595                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22596                && matches!(self.tokens.get(self.pos + 2),
22597                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22598            {
22599                self.advance(); // AT
22600                self.advance(); // TIME
22601                self.advance(); // ZONE
22602                // Zone at comparison precedence so AND/OR stay out.
22603                let zone = self.parse_expr(6)?;
22604                expr = Expr::FunctionCall {
22605                    name: "timezone".to_string(),
22606                    args: alloc::vec![zone, expr],
22607                };
22608                continue;
22609            }
22610            // `expr COLLATE "name"` — SPG's single text ordering IS
22611            // byte order, i.e. the C collation. The byte-order
22612            // spellings absorb as no-ops; a locale collation would
22613            // silently sort differently from PG, so it errors
22614            // honestly instead.
22615            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22616                self.advance();
22617                let mut cname = match self.advance() {
22618                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22619                    other => {
22620                        return Err(self.err(alloc::format!(
22621                            "expected collation name after COLLATE, got {other:?}"
22622                        )));
22623                    }
22624                };
22625                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22626                // is how `pg_dump` writes the default one:
22627                // `… COLLATE pg_catalog.default`. Reading a single token
22628                // left the SCHEMA as the name, so the clause was refused
22629                // as an unsupported locale collation and no dump ran.
22630                if matches!(self.peek(), Token::Dot) {
22631                    self.advance();
22632                    cname = match self.advance() {
22633                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22634                        // `default` lexes as a KEYWORD, and it is the name
22635                        // pg_dump writes — the same trap round 535 hit with
22636                        // TABLE / INDEX / FULL.
22637                        Token::Default => alloc::string::String::from("default"),
22638                        other => {
22639                            return Err(self.err(alloc::format!(
22640                                "expected collation name after COLLATE, got {other:?}"
22641                            )));
22642                        }
22643                    };
22644                }
22645                let lc = cname.to_ascii_lowercase();
22646                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22647                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22648                // family / `binary`) forces byte-wise, which is exactly
22649                // what `BINARY expr` does — lower onto that so every fold
22650                // site (comparison, LIKE, ORDER BY) suppresses via
22651                // `is_binary_coerced`. A `_ci` family override folds, and
22652                // under the MySQL dialect the default already folds, so it
22653                // absorbs as a no-op; likewise the C / byte-order spellings.
22654                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22655                    expr = Expr::Cast {
22656                        expr: alloc::boxed::Box::new(expr),
22657                        target: CastTarget::Named("binary".to_string()),
22658                    };
22659                    continue;
22660                }
22661                let mysql_ci = self.mysql_dialect
22662                    && (lc.ends_with("_ci")
22663                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22664                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22665                // goes to the lowering channel, the byte-order spellings
22666                // included. Round 691 recorded only the names the old
22667                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22668                // absorbed as a no-op — and once a column could declare a
22669                // collation, absorbing the clause meant the COLUMN's
22670                // collation won where the query had asked for bytes.
22671                if self.in_order_by_key && !mysql_ci {
22672                    self.order_key_collation = Some(cname);
22673                    continue;
22674                }
22675                if !matches!(
22676                    lc.as_str(),
22677                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22678                ) && !mysql_ci
22679                {
22680                    // v7.38.18 — the old message read "SPG orders text
22681                    // by bytes (the C collation); locale collations are
22682                    // not supported yet", and both halves were false by
22683                    // the time it was read. This build performs locale
22684                    // collations: declared on a column or written in an
22685                    // ORDER BY key, `en_US.utf8` orders `apple, client,
22686                    // DateStyle, Zebra` exactly as PG 18.4 does. What it
22687                    // cannot do is carry a collation on an arbitrary
22688                    // expression, because there is no `Expr::Collate` to
22689                    // carry it — so say that, and say where the clause
22690                    // does work rather than telling the reader to drop it.
22691                    return Err(self.err(alloc::format!(
22692                        "COLLATE {cname:?} is not supported in this position: \
22693                         SPG carries a collation on a column declaration and \
22694                         on an ORDER BY key, not on an arbitrary expression. \
22695                         Declare it on the column (`x text COLLATE \
22696                         {cname:?}`) or move it into the ORDER BY key"
22697                    )));
22698                }
22699                continue;
22700            }
22701            return Ok(expr);
22702        }
22703    }
22704
22705    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22706    /// the first token that is not one. Schema qualifiers collapse to the
22707    /// last part, which is what every other name path here does (SPG is
22708    /// single-schema).
22709    fn take_comma_separated_names(&mut self) -> Vec<String> {
22710        let mut out = Vec::new();
22711        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22712            self.advance();
22713            let mut last = n;
22714            while matches!(self.peek(), Token::Dot) {
22715                self.advance();
22716                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22717                    last = t;
22718                }
22719            }
22720            out.push(last);
22721            if matches!(self.peek(), Token::Comma) {
22722                self.advance();
22723            } else {
22724                break;
22725            }
22726        }
22727        out
22728    }
22729
22730    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22731    ///
22732    /// The general cast-target path tests this inline; the types with their
22733    /// own `CastTarget` variant need it as a guard on their match arm,
22734    /// which is what this exists for.
22735    fn peek_postfix_array_brackets(&self) -> bool {
22736        matches!(self.peek(), Token::LBracket)
22737            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22738    }
22739
22740    /// Parse the operator tail after a `(a, b, …)` row constructor
22741    /// and expand at parse time. `=` is the conjunction of element
22742    /// equalities; `<>` its negation; the order operators expand
22743    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22744    /// equalities. Anything else (a bare row value, a subquery
22745    /// RHS) errors honestly — SPG has no composite runtime value.
22746    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22747        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22748            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22749                lhs: Box::new(l.clone()),
22750                op: BinOp::Eq,
22751                rhs: Box::new(r.clone()),
22752            });
22753            let first = it.next().expect("row has at least two elements");
22754            it.fold(first, |acc, e| Expr::Binary {
22755                lhs: Box::new(acc),
22756                op: BinOp::And,
22757                rhs: Box::new(e),
22758            })
22759        }
22760        // Lexicographic (a,b) OP (c,d):
22761        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22762        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22763            if lhs.len() == 1 {
22764                return Expr::Binary {
22765                    lhs: Box::new(lhs[0].clone()),
22766                    op: last,
22767                    rhs: Box::new(rhs[0].clone()),
22768                };
22769            }
22770            let head_strict = Expr::Binary {
22771                lhs: Box::new(lhs[0].clone()),
22772                op: strict,
22773                rhs: Box::new(rhs[0].clone()),
22774            };
22775            let head_eq = Expr::Binary {
22776                lhs: Box::new(lhs[0].clone()),
22777                op: BinOp::Eq,
22778                rhs: Box::new(rhs[0].clone()),
22779            };
22780            Expr::Binary {
22781                lhs: Box::new(head_strict),
22782                op: BinOp::Or,
22783                rhs: Box::new(Expr::Binary {
22784                    lhs: Box::new(head_eq),
22785                    op: BinOp::And,
22786                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22787                }),
22788            }
22789        }
22790        let negated_in = if matches!(self.peek(), Token::Not)
22791            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22792        {
22793            self.advance();
22794            true
22795        } else {
22796            false
22797        };
22798        if matches!(self.peek(), Token::In) {
22799            self.advance();
22800            if !matches!(self.peek(), Token::LParen) {
22801                return Err(self.err(alloc::format!(
22802                    "expected '(' after row IN, got {:?}",
22803                    self.peek()
22804                )));
22805            }
22806            self.advance();
22807            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22808            // not a list of literal rows. Row-vs-list decomposes to
22809            // OR-of-AND above, but the subquery's rows are only known at
22810            // runtime, so keep it as a RowInSubquery node.
22811            if matches!(self.peek(), Token::Select) {
22812                let inner = self.parse_select_stmt()?;
22813                if !matches!(self.peek(), Token::RParen) {
22814                    return Err(self.err(alloc::format!(
22815                        "expected ')' after row IN-subquery, got {:?}",
22816                        self.peek()
22817                    )));
22818                }
22819                self.advance();
22820                let Statement::Select(s) = inner else {
22821                    unreachable!("parse_select_stmt always returns Statement::Select")
22822                };
22823                return Ok(Expr::RowInSubquery {
22824                    row,
22825                    subquery: Box::new(s),
22826                    negated: negated_in,
22827                });
22828            }
22829            let mut alternatives: Vec<Expr> = Vec::new();
22830            loop {
22831                // Optional ROW keyword before the paren row.
22832                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22833                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22834                {
22835                    self.advance();
22836                }
22837                if !matches!(self.peek(), Token::LParen) {
22838                    return Err(self.err(alloc::format!(
22839                        "expected '(' to open a row inside IN, got {:?}",
22840                        self.peek()
22841                    )));
22842                }
22843                self.advance();
22844                let mut rhs = alloc::vec![self.parse_expr(0)?];
22845                while matches!(self.peek(), Token::Comma) {
22846                    self.advance();
22847                    rhs.push(self.parse_expr(0)?);
22848                }
22849                if !matches!(self.peek(), Token::RParen) {
22850                    return Err(self.err(alloc::format!(
22851                        "expected ')' after row inside IN, got {:?}",
22852                        self.peek()
22853                    )));
22854                }
22855                self.advance();
22856                if rhs.len() != row.len() {
22857                    return Err(self.err(alloc::format!(
22858                        "row IN arity mismatch: left has {}, right has {}",
22859                        row.len(),
22860                        rhs.len()
22861                    )));
22862                }
22863                alternatives.push(row_eq(&row, &rhs));
22864                if matches!(self.peek(), Token::Comma) {
22865                    self.advance();
22866                    continue;
22867                }
22868                break;
22869            }
22870            if !matches!(self.peek(), Token::RParen) {
22871                return Err(self.err(alloc::format!(
22872                    "expected ')' to close row IN list, got {:?}",
22873                    self.peek()
22874                )));
22875            }
22876            self.advance();
22877            let mut it = alternatives.into_iter();
22878            let first = it.next().expect("IN list has at least one row");
22879            let combined = it.fold(first, |acc, e| Expr::Binary {
22880                lhs: Box::new(acc),
22881                op: BinOp::Or,
22882                rhs: Box::new(e),
22883            });
22884            return Ok(maybe_not(combined, negated_in));
22885        }
22886        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22887        // two periods share at least one time point. Each pair is
22888        // normalised with least/greatest (PG accepts the endpoints
22889        // in either order), then lowered to the standard
22890        // `start1 < end2 AND start2 < end1` form.
22891        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22892            if row.len() != 2 {
22893                return Err(self.err(alloc::format!(
22894                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
22895                    row.len()
22896                )));
22897            }
22898            self.advance();
22899            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22900                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22901            {
22902                self.advance();
22903            }
22904            if !matches!(self.peek(), Token::LParen) {
22905                return Err(self.err(alloc::format!(
22906                    "expected '(' after OVERLAPS, got {:?}",
22907                    self.peek()
22908                )));
22909            }
22910            self.advance();
22911            let r0 = self.parse_expr(0)?;
22912            if !matches!(self.peek(), Token::Comma) {
22913                return Err(self.err(alloc::format!(
22914                    "OVERLAPS needs (start, end) on the right, got {:?}",
22915                    self.peek()
22916                )));
22917            }
22918            self.advance();
22919            let r1 = self.parse_expr(0)?;
22920            if !matches!(self.peek(), Token::RParen) {
22921                return Err(self.err(alloc::format!(
22922                    "expected ')' after OVERLAPS pair, got {:?}",
22923                    self.peek()
22924                )));
22925            }
22926            self.advance();
22927            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22928                name: String::from(name),
22929                args: alloc::vec![a.clone(), b.clone()],
22930            };
22931            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22932                lhs: Box::new(lhs),
22933                op: BinOp::Lt,
22934                rhs: Box::new(rhs),
22935            };
22936            return Ok(Expr::Binary {
22937                lhs: Box::new(lt(
22938                    pair_fn("least", &row[0], &row[1]),
22939                    pair_fn("greatest", &r0, &r1),
22940                )),
22941                op: BinOp::And,
22942                rhs: Box::new(lt(
22943                    pair_fn("least", &r0, &r1),
22944                    pair_fn("greatest", &row[0], &row[1]),
22945                )),
22946            });
22947        }
22948        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22949        // PG, `IS NULL` is true only when EVERY field is NULL, and
22950        // `IS NOT NULL` is true only when every field is non-NULL — the
22951        // latter is NOT the negation of the former (a mixed row is
22952        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22953        // which reproduces exactly that all-fields semantics.
22954        if matches!(self.peek(), Token::Is) {
22955            self.advance();
22956            let negated = if matches!(self.peek(), Token::Not) {
22957                self.advance();
22958                true
22959            } else {
22960                false
22961            };
22962            if !matches!(self.peek(), Token::Null) {
22963                return Err(self.err(alloc::format!(
22964                    "expected NULL after row IS [NOT], got {:?}",
22965                    self.peek()
22966                )));
22967            }
22968            self.advance();
22969            let mut it = row.iter().map(|e| Expr::IsNull {
22970                expr: Box::new(e.clone()),
22971                negated,
22972            });
22973            let first = it.next().expect("row has at least two elements");
22974            return Ok(it.fold(first, |acc, e| Expr::Binary {
22975                lhs: Box::new(acc),
22976                op: BinOp::And,
22977                rhs: Box::new(e),
22978            }));
22979        }
22980        let op = match self.peek() {
22981            Token::Eq => BinOp::Eq,
22982            Token::NotEq => BinOp::NotEq,
22983            Token::Lt => BinOp::Lt,
22984            Token::LtEq => BinOp::LtEq,
22985            Token::Gt => BinOp::Gt,
22986            Token::GtEq => BinOp::GtEq,
22987            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22988            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22989            // constructor value, identical to the `ROW(a, b, …)` keyword form:
22990            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22991            // (`::text`, `.field`) applies at the caller just as it does for the
22992            // ROW(...) node. All the comparison / predicate forms returned above.
22993            _ => {
22994                return Ok(Expr::FunctionCall {
22995                    name: String::from("row"),
22996                    args: row,
22997                });
22998            }
22999        };
23000        self.advance();
23001        // Optional ROW keyword before the paren row.
23002        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23003            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23004        {
23005            self.advance();
23006        }
23007        if !matches!(self.peek(), Token::LParen) {
23008            return Err(self.err(alloc::format!(
23009                "expected '(' to open the right-hand row, got {:?}",
23010                self.peek()
23011            )));
23012        }
23013        self.advance();
23014        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23015        // subquery. Kept as a node (the subquery's row is a runtime value);
23016        // the literal-RHS form below still decomposes at parse time.
23017        if matches!(self.peek(), Token::Select) {
23018            let inner = self.parse_select_stmt()?;
23019            if !matches!(self.peek(), Token::RParen) {
23020                return Err(self.err(alloc::format!(
23021                    "expected ')' after row comparison subquery, got {:?}",
23022                    self.peek()
23023                )));
23024            }
23025            self.advance();
23026            let Statement::Select(s) = inner else {
23027                unreachable!("parse_select_stmt always returns Statement::Select")
23028            };
23029            return Ok(Expr::RowCmpSubquery {
23030                row,
23031                op,
23032                subquery: Box::new(s),
23033            });
23034        }
23035        let mut rhs = alloc::vec![self.parse_expr(0)?];
23036        while matches!(self.peek(), Token::Comma) {
23037            self.advance();
23038            rhs.push(self.parse_expr(0)?);
23039        }
23040        if !matches!(self.peek(), Token::RParen) {
23041            return Err(self.err(alloc::format!(
23042                "expected ')' after right-hand row, got {:?}",
23043                self.peek()
23044            )));
23045        }
23046        self.advance();
23047        if rhs.len() != row.len() {
23048            // v7.39 (round 239) — PG's wording (42601).
23049            return Err(self.err("unequal number of entries in row expressions".to_string()));
23050        }
23051        Ok(match op {
23052            BinOp::Eq => row_eq(&row, &rhs),
23053            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23054            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23055            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23056            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23057            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23058            _ => unreachable!("op restricted above"),
23059        })
23060    }
23061
23062    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23063    /// escape character becomes the matcher's default backslash:
23064    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23065    /// → the char itself, and any pre-existing backslash escapes
23066    /// itself so it stays literal. Both operands must be string
23067    /// literals — a runtime pattern would need matcher support.
23068    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23069        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23070            (&pattern, &esc)
23071        else {
23072            return Err(
23073                "LIKE ... ESCAPE requires string-literal pattern and escape \
23074                 (runtime escape characters are not supported yet)"
23075                    .into(),
23076            );
23077        };
23078        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23079        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23080        // multi-character escape is an error.
23081        let esc_ch: Option<char> = {
23082            let mut ch_iter = e.chars();
23083            match (ch_iter.next(), ch_iter.next()) {
23084                (Some(c), None) => Some(c),
23085                (None, _) => None,
23086                (Some(_), Some(_)) => {
23087                    return Err(alloc::format!(
23088                        "ESCAPE must be a single character, got {e:?}"
23089                    ));
23090                }
23091            }
23092        };
23093        let mut out = String::with_capacity(p.len() + 4);
23094        let mut chars = p.chars();
23095        while let Some(c) = chars.next() {
23096            if Some(c) == esc_ch {
23097                match chars.next() {
23098                    // Escaped wildcard / escaped escape → keep the
23099                    // next char literal via backslash.
23100                    Some(next) => {
23101                        out.push('\\');
23102                        out.push(next);
23103                    }
23104                    None => {
23105                        return Err("LIKE pattern ends with the escape character".into());
23106                    }
23107                }
23108            } else if c == '\\' && esc_ch != Some('\\') {
23109                // A raw backslash is literal under a custom (or absent) escape
23110                // — escape it for the backslash-based matcher.
23111                out.push('\\');
23112                out.push('\\');
23113            } else {
23114                out.push(c);
23115            }
23116        }
23117        Ok(Expr::Literal(Literal::String(out)))
23118    }
23119
23120    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23121    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23122    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23123    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23124    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23125    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23126    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23127    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23128    /// array expression errors honestly rather than silently mismatching.
23129    fn try_like_any_all(
23130        &mut self,
23131        base: &Expr,
23132        negated: bool,
23133        case_insensitive: bool,
23134    ) -> Result<Option<Expr>, ParseError> {
23135        let is_any = match self.peek() {
23136            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23137            Token::Ident(s)
23138                if s.eq_ignore_ascii_case("any")
23139                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23140            {
23141                true
23142            }
23143            _ => return Ok(None),
23144        };
23145        self.advance(); // ANY / ALL
23146        self.advance(); // '('
23147        let arr = self.parse_expr(0)?;
23148        if !matches!(self.peek(), Token::RParen) {
23149            return Err(self.err(format!(
23150                "expected ')' after LIKE {} argument, got {:?}",
23151                if is_any { "ANY" } else { "ALL" },
23152                self.peek()
23153            )));
23154        }
23155        self.advance(); // ')'
23156        let Expr::Array(items) = arr else {
23157            return Err(self.err(
23158                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23159            ));
23160        };
23161        let mut clauses = items.into_iter().map(|p| Expr::Like {
23162            expr: Box::new(base.clone()),
23163            pattern: Box::new(p),
23164            negated,
23165            case_insensitive,
23166        });
23167        let Some(first) = clauses.next() else {
23168            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23169            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23170        };
23171        let op = if is_any { BinOp::Or } else { BinOp::And };
23172        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23173            lhs: Box::new(acc),
23174            op,
23175            rhs: Box::new(c),
23176        });
23177        Ok(Some(combined))
23178    }
23179
23180    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23181    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23182    /// `AND` is not swallowed.
23183    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23184        self.advance(); // BETWEEN
23185        // SYMMETRIC — the bounds may arrive in either order; both
23186        // orientations OR together. ASYMMETRIC is the default and
23187        // absorbs as noise.
23188        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23189        {
23190            self.advance();
23191            true
23192        } else {
23193            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23194                self.advance();
23195            }
23196            false
23197        };
23198        let low = self.parse_expr(6)?;
23199        if !matches!(self.peek(), Token::And) {
23200            return Err(self.err(format!(
23201                "expected AND after BETWEEN low bound, got {:?}",
23202                self.peek()
23203            )));
23204        }
23205        self.advance();
23206        let high = self.parse_expr(6)?;
23207        let target = Box::new(expr);
23208        let range = |lo: Expr, hi: Expr| Expr::Binary {
23209            lhs: Box::new(Expr::Binary {
23210                lhs: target.clone(),
23211                op: BinOp::GtEq,
23212                rhs: Box::new(lo),
23213            }),
23214            op: BinOp::And,
23215            rhs: Box::new(Expr::Binary {
23216                lhs: target.clone(),
23217                op: BinOp::LtEq,
23218                rhs: Box::new(hi),
23219            }),
23220        };
23221        let combined = if symmetric {
23222            Expr::Binary {
23223                lhs: Box::new(range(low.clone(), high.clone())),
23224                op: BinOp::Or,
23225                rhs: Box::new(range(high, low)),
23226            }
23227        } else {
23228            range(low, high)
23229        };
23230        Ok(maybe_not(combined, negated))
23231    }
23232
23233    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23234    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23235    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23236    /// Caller already consumed the leading `WITH` ident.
23237    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23238    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23239    /// self-reference that appears more than once in a single term.
23240    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23241        use crate::ast::{CteBody, SelectStatement};
23242        if !cte.recursive {
23243            return Ok(());
23244        }
23245        let CteBody::Select(body) = &cte.body else {
23246            return Ok(());
23247        };
23248        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23249        // check the anchor and every peer term.
23250        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23251        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23252        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23253            return Err(self.err(String::from(
23254                "ORDER BY in a recursive query is not implemented",
23255            )));
23256        }
23257        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23258            return Err(self.err(String::from(
23259                "LIMIT in a recursive query is not implemented",
23260            )));
23261        }
23262        let self_refs = |s: &SelectStatement| -> usize {
23263            let Some(from) = &s.from else {
23264                return 0;
23265            };
23266            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23267            for j in &from.joins {
23268                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23269                    n += 1;
23270                }
23271            }
23272            n
23273        };
23274        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23275            return Err(self.err(alloc::format!(
23276                "recursive reference to query \"{}\" must not appear more than once",
23277                cte.name
23278            )));
23279        }
23280        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23281        // apply only when the body actually references itself (a non-self-
23282        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23283        let anchor_refs = self_refs(body);
23284        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23285        if anchor_refs > 0 || union_refs {
23286            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23287            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23288            // "does not have the form" error — SPG used to compute a value.
23289            if body.unions.is_empty()
23290                || body.unions.iter().any(|(k, _)| {
23291                    !matches!(
23292                        k,
23293                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23294                    )
23295                })
23296            {
23297                return Err(self.err(alloc::format!(
23298                    "recursive query \"{}\" does not have the form non-recursive-term \
23299                     UNION [ALL] recursive-term",
23300                    cte.name
23301                )));
23302            }
23303            if anchor_refs > 0 {
23304                return Err(self.err(alloc::format!(
23305                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23306                    cte.name
23307                )));
23308            }
23309        }
23310        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23311        for (_, u) in &body.unions {
23312            if self_refs(u) == 0 {
23313                continue;
23314            }
23315            // The self-reference must not sit on the nullable side of an outer
23316            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23317            if let Some(from) = &u.from {
23318                for (i, j) in from.joins.iter().enumerate() {
23319                    let left_has_self = is_self(&from.primary)
23320                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23321                    let violated = match j.kind {
23322                        crate::ast::JoinKind::Left => is_self(&j.table),
23323                        crate::ast::JoinKind::Right => left_has_self,
23324                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23325                        _ => false,
23326                    };
23327                    if violated {
23328                        return Err(self.err(alloc::format!(
23329                            "recursive reference to query \"{}\" must not appear within an outer join",
23330                            cte.name
23331                        )));
23332                    }
23333                }
23334            }
23335            // No aggregates at the top level of the recursive term (SPG used
23336            // to run them and surface a misleading downstream error).
23337            let mut items_and_having: Vec<&Expr> = Vec::new();
23338            for it in &u.items {
23339                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23340                    items_and_having.push(expr);
23341                }
23342            }
23343            if let Some(h) = &u.having {
23344                items_and_having.push(h);
23345            }
23346            for e in items_and_having {
23347                if expr_has_toplevel_aggregate(e) {
23348                    return Err(self.err(String::from(
23349                        "aggregate functions are not allowed in a recursive query's recursive term",
23350                    )));
23351                }
23352            }
23353        }
23354        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23355        // subquery) anywhere in the body is rejected; a plain FROM derived
23356        // table is legal in PG and untouched here.
23357        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23358        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23359        for term in all_terms {
23360            if select_has_self_ref_in_sublink(term, &cte.name) {
23361                return Err(self.err(alloc::format!(
23362                    "recursive reference to query \"{}\" must not appear within a subquery",
23363                    cte.name
23364                )));
23365            }
23366        }
23367        Ok(())
23368    }
23369
23370    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23371    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23372    /// right after parse so the engine sees a plain recursive CTE with the
23373    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23374    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23375    /// text-rendered rows can't provide, and errors honestly.
23376    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23377        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23378        if cte.search.is_none() && cte.cycle.is_none() {
23379            return Ok(());
23380        }
23381        let cte_name = cte.name.clone();
23382        let col_names = cte.column_overrides.clone();
23383        if col_names.is_empty() {
23384            return Err(
23385                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23386            );
23387        }
23388        let search = cte.search.take();
23389        let cycle = cte.cycle.take();
23390        let mut extra_cols: Vec<String> = Vec::new();
23391        let col_ref = |name: &str| {
23392            Expr::Column(ColumnName {
23393                qualifier: Some(cte_name.clone()),
23394                name: name.to_string(),
23395            })
23396        };
23397        // Position of a SEARCH/CYCLE column within the CTE's column list.
23398        let pos_of = |name: &str| -> Result<usize, ParseError> {
23399            col_names
23400                .iter()
23401                .position(|c| c.eq_ignore_ascii_case(name))
23402                .ok_or_else(|| {
23403                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23404                })
23405        };
23406        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23407            let mut args = Vec::with_capacity(positions.len());
23408            for &p in positions {
23409                match items.get(p) {
23410                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23411                    _ => {
23412                        return Err(self.err(
23413                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23414                        ));
23415                    }
23416                }
23417            }
23418            Ok(Expr::FunctionCall {
23419                name: "row".into(),
23420                args,
23421            })
23422        };
23423        let CteBody::Select(body) = &mut cte.body else {
23424            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23425        };
23426        if body.unions.is_empty() {
23427            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23428        }
23429        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23430
23431        if let Some(srch) = search {
23432            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23433            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23434            // no typed `record[]`, but element-wise array ORDER BY is correct
23435            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23436            // exactly onto a typed array: DEPTH is the root→node path
23437            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23438            // orders numerically (multi-digit keys included), matching PG.
23439            //
23440            // A multi-column BY would need a record[] to keep the per-node key
23441            // tuple orderable, which SPG can't express — error honestly there
23442            // rather than mis-order.
23443            if srch.by_columns.len() != 1 {
23444                return Err(self.err(
23445                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23446                     SPG doesn't have yet; a single BY column is supported"
23447                        .into(),
23448                ));
23449            }
23450            let key_pos = pos_of(&srch.by_columns[0])?;
23451            let base_key = match body.items.get(key_pos) {
23452                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23453                _ => {
23454                    return Err(
23455                        self.err("SEARCH BY column maps to a non-expression select item".into())
23456                    );
23457                }
23458            };
23459            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23460                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23461                _ => {
23462                    return Err(
23463                        self.err("SEARCH BY column maps to a non-expression select item".into())
23464                    );
23465                }
23466            };
23467            if srch.depth_first {
23468                // base: ARRAY[key]; rec: array_append(cte.set, key).
23469                body.items.push(SelectItem::Expr {
23470                    expr: Expr::Array(alloc::vec![base_key]),
23471                    alias: Some(srch.set_column.clone()),
23472                });
23473                body.unions[rec].1.items.push(SelectItem::Expr {
23474                    expr: Expr::FunctionCall {
23475                        name: "array_append".into(),
23476                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23477                    },
23478                    alias: Some(srch.set_column.clone()),
23479                });
23480            } else {
23481                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23482                // leading depth element dominates the element-wise comparison,
23483                // so shallower rows sort first, then by key — PG's (depth, key).
23484                body.items.push(SelectItem::Expr {
23485                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23486                    alias: Some(srch.set_column.clone()),
23487                });
23488                // rec depth = cte.set[1] + 1.
23489                let parent_depth = Expr::ArraySubscript {
23490                    target: Box::new(col_ref(&srch.set_column)),
23491                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23492                };
23493                body.unions[rec].1.items.push(SelectItem::Expr {
23494                    expr: Expr::Array(alloc::vec![
23495                        Expr::Binary {
23496                            lhs: Box::new(parent_depth),
23497                            op: BinOp::Add,
23498                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23499                        },
23500                        rec_key,
23501                    ]),
23502                    alias: Some(srch.set_column.clone()),
23503                });
23504            }
23505            extra_cols.push(srch.set_column);
23506        }
23507
23508        if let Some(cyc) = cycle {
23509            let positions: Vec<usize> = cyc
23510                .columns
23511                .iter()
23512                .map(|c| pos_of(c))
23513                .collect::<Result<_, _>>()?;
23514            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23515            // cast it to text for the cycle path: membership only needs equality,
23516            // and the record text form gives SPG a TextArray path (SPG has no
23517            // typed record[] array). Cycle detection is unaffected.
23518            let base_row = Expr::Cast {
23519                expr: Box::new(row_of(&body.items, &positions)?),
23520                target: CastTarget::Text,
23521            };
23522            let rec_row = Expr::Cast {
23523                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23524                target: CastTarget::Text,
23525            };
23526            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23527            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23528            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23529            body.items.push(SelectItem::Expr {
23530                expr: Expr::Literal(dflt.clone()),
23531                alias: Some(cyc.mark_column.clone()),
23532            });
23533            body.items.push(SelectItem::Expr {
23534                expr: Expr::Array(alloc::vec![base_row]),
23535                alias: Some(cyc.path_column.clone()),
23536            });
23537            // rec mark: ROW(cols) already in the path → cycle.
23538            let hit = Expr::AnyAll {
23539                expr: Box::new(rec_row.clone()),
23540                op: BinOp::Eq,
23541                array: Box::new(col_ref(&cyc.path_column)),
23542                is_any: true,
23543            };
23544            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23545                Expr::Case {
23546                    operand: None,
23547                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23548                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23549                }
23550            } else {
23551                hit
23552            };
23553            body.unions[rec].1.items.push(SelectItem::Expr {
23554                expr: mark_expr,
23555                alias: Some(cyc.mark_column.clone()),
23556            });
23557            // rec path: array_append(cte.path, ROW(cols)).
23558            body.unions[rec].1.items.push(SelectItem::Expr {
23559                expr: Expr::FunctionCall {
23560                    name: "array_append".into(),
23561                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23562                },
23563                alias: Some(cyc.path_column.clone()),
23564            });
23565            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23566            let stop = Expr::Unary {
23567                op: UnOp::Not,
23568                expr: Box::new(col_ref(&cyc.mark_column)),
23569            };
23570            let w = &mut body.unions[rec].1.where_;
23571            *w = Some(match w.take() {
23572                Some(prev) => Expr::Binary {
23573                    lhs: Box::new(prev),
23574                    op: BinOp::And,
23575                    rhs: Box::new(stop),
23576                },
23577                None => stop,
23578            });
23579            extra_cols.push(cyc.mark_column);
23580            extra_cols.push(cyc.path_column);
23581        }
23582        cte.column_overrides.extend(extra_cols);
23583        Ok(())
23584    }
23585
23586    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23587    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23588    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23589        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23590            return Ok(None);
23591        }
23592        self.advance(); // SEARCH
23593        let depth_first = match self.peek() {
23594            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23595            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23596            other => {
23597                return Err(self.err(format!(
23598                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23599                )));
23600            }
23601        };
23602        self.advance();
23603        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23604            return Err(self.err(format!(
23605                "expected FIRST after SEARCH mode, got {:?}",
23606                self.peek()
23607            )));
23608        }
23609        self.advance();
23610        if !self.peek_is_by() {
23611            return Err(self.err(format!(
23612                "expected BY after SEARCH … FIRST, got {:?}",
23613                self.peek()
23614            )));
23615        }
23616        self.advance();
23617        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23618        while matches!(self.peek(), Token::Comma) {
23619            self.advance();
23620            by_columns.push(self.expect_ident_like()?);
23621        }
23622        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23623            return Err(self.err(format!(
23624                "expected SET in SEARCH clause, got {:?}",
23625                self.peek()
23626            )));
23627        }
23628        self.advance();
23629        let set_column = self.expect_ident_like()?;
23630        Ok(Some(crate::ast::SearchClause {
23631            depth_first,
23632            by_columns,
23633            set_column,
23634        }))
23635    }
23636
23637    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23638    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23639    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23640        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23641            return Ok(None);
23642        }
23643        self.advance(); // CYCLE
23644        let mut columns = alloc::vec![self.expect_ident_like()?];
23645        while matches!(self.peek(), Token::Comma) {
23646            self.advance();
23647            columns.push(self.expect_ident_like()?);
23648        }
23649        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23650            return Err(self.err(format!(
23651                "expected SET in CYCLE clause, got {:?}",
23652                self.peek()
23653            )));
23654        }
23655        self.advance();
23656        let mark_column = self.expect_ident_like()?;
23657        let mut mark_value = None;
23658        let mut default_value = None;
23659        if matches!(self.peek(), Token::To) {
23660            self.advance();
23661            mark_value = Some(self.parse_cycle_literal()?);
23662            if !matches!(self.peek(), Token::Default) {
23663                return Err(self.err(format!(
23664                    "expected DEFAULT after CYCLE … TO, got {:?}",
23665                    self.peek()
23666                )));
23667            }
23668            self.advance();
23669            default_value = Some(self.parse_cycle_literal()?);
23670        }
23671        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23672            return Err(self.err(format!(
23673                "expected USING in CYCLE clause, got {:?}",
23674                self.peek()
23675            )));
23676        }
23677        self.advance();
23678        let path_column = self.expect_ident_like()?;
23679        Ok(Some(crate::ast::CycleClause {
23680            columns,
23681            mark_column,
23682            mark_value,
23683            default_value,
23684            path_column,
23685        }))
23686    }
23687
23688    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23689    /// literal (string / bool / number) in PG.
23690    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23691        match self.parse_expr(0)? {
23692            Expr::Literal(l) => Ok(l),
23693            other => Err(self.err(format!(
23694                "CYCLE mark/default value must be a literal, got {other:?}"
23695            ))),
23696        }
23697    }
23698
23699    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23700        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23701        // Comes through as an identifier; consume it if present and
23702        // mark every CTE in the clause as recursive (PG semantics —
23703        // the flag is per-WITH, not per-CTE).
23704        let mut recursive = false;
23705        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23706            && s.eq_ignore_ascii_case("recursive")
23707        {
23708            self.advance();
23709            recursive = true;
23710        }
23711        let mut ctes = Vec::new();
23712        loop {
23713            let name = self.expect_ident_like()?;
23714            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23715            // PG uses these to rename the body's output columns; we
23716            // do the same below by overriding `columns[i].name`.
23717            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23718                self.advance();
23719                let mut names = Vec::new();
23720                loop {
23721                    names.push(self.expect_ident_like()?);
23722                    if matches!(self.peek(), Token::Comma) {
23723                        self.advance();
23724                        continue;
23725                    }
23726                    break;
23727                }
23728                if !matches!(self.peek(), Token::RParen) {
23729                    return Err(self.err(format!(
23730                        "expected ')' to close CTE column list, got {:?}",
23731                        self.peek()
23732                    )));
23733                }
23734                self.advance();
23735                names
23736            } else {
23737                Vec::new()
23738            };
23739            // AS is a reserved Token::As (used by SELECT-item / FROM
23740            // aliasing) — handle it specially rather than as a bare
23741            // ident.
23742            if !matches!(self.peek(), Token::As) {
23743                return Err(self.err(format!(
23744                    "expected AS after CTE name {name:?}, got {:?}",
23745                    self.peek()
23746                )));
23747            }
23748            self.advance();
23749            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23750            // MATERIALIZED` optimizer hints. SPG materialises every
23751            // CTE, so both spellings are accepted and absorbed.
23752            if matches!(self.peek(), Token::Not) {
23753                self.advance(); // NOT
23754                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23755                    if s.eq_ignore_ascii_case("materialized"))
23756                {
23757                    self.advance();
23758                } else {
23759                    return Err(self.err(format!(
23760                        "expected MATERIALIZED after AS NOT, got {:?}",
23761                        self.peek()
23762                    )));
23763                }
23764            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23765                if s.eq_ignore_ascii_case("materialized"))
23766            {
23767                self.advance();
23768            }
23769            if !matches!(self.peek(), Token::LParen) {
23770                return Err(self.err(format!(
23771                    "expected '(' after AS in WITH clause, got {:?}",
23772                    self.peek()
23773                )));
23774            }
23775            self.advance();
23776            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23777            // RETURNING) as the CTE body in addition to SELECT.
23778            // PG writable CTE semantics. UPDATE / DELETE come in as
23779            // bare Idents (lexer keeps SELECT / INSERT as reserved
23780            // tokens but treats the rest of DML as case-insensitive
23781            // idents).
23782            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23783            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23784            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23785            let body = match self.peek() {
23786                Token::Select => {
23787                    let inner = self.parse_select_stmt()?;
23788                    let Statement::Select(s) = inner else {
23789                        unreachable!("parse_select_stmt returns Select");
23790                    };
23791                    crate::ast::CteBody::Select(s)
23792                }
23793                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23794                // `SELECT * FROM t` this way and accepts it wherever a
23795                // SELECT goes, so the CTE body dispatch needs its own
23796                // arm: this match is keyed on the FIRST token, and
23797                // `Token::Table` fell through to a tail that then
23798                // rejected what it got. `parse_table_shorthand` has
23799                // returned a desugared SelectStatement since the
23800                // shorthand landed — only the routing was missing.
23801                // Round 868 found this by putting the shorthand in a
23802                // subquery; every earlier check used a top-level form.
23803                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23804                // `SELECT * FROM t` this way and accepts it wherever a
23805                // SELECT goes, so the CTE body dispatch needs its own
23806                // arm: this match is keyed on the FIRST token, and
23807                // `Token::Table` fell through to a tail that rejected
23808                // what it got. `parse_table_shorthand` has returned a
23809                // desugared SelectStatement since the shorthand landed —
23810                // only the routing was missing, here and in the derived
23811                // table's second-token gate. Round 868 found both by
23812                // putting the shorthand in a subquery; every earlier
23813                // check had used a top-level form.
23814                Token::Table
23815                    if matches!(
23816                        self.tokens.get(self.pos + 1),
23817                        Some(Token::Ident(_) | Token::QuotedIdent(_))
23818                    ) =>
23819                {
23820                    let mut head = self.parse_table_shorthand()?;
23821                    self.parse_setop_chain_into(&mut head)?;
23822                    self.parse_select_tail_into(&mut head)?;
23823                    crate::ast::CteBody::Select(head)
23824                }
23825                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23826                // WITH t(a) AS (VALUES (1), (2)) … lowers through
23827                // the shared rows helper onto a Select body.
23828                Token::Values => {
23829                    self.advance(); // VALUES
23830                    let mut head = self.parse_values_rows_body()?;
23831                    // A VALUES seed can head a set-operation chain —
23832                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23833                    // SELECT n+1 FROM t …). Attach any trailing
23834                    // UNION / INTERSECT / EXCEPT peers so the
23835                    // recursive-CTE body parses like the SELECT seed.
23836                    self.parse_setop_chain_into(&mut head)?;
23837                    crate::ast::CteBody::Select(head)
23838                }
23839                Token::Insert => {
23840                    let inner = self.parse_one_statement()?;
23841                    let Statement::Insert(s) = inner else {
23842                        unreachable!("Token::Insert routes to Insert");
23843                    };
23844                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23845                }
23846                _ if is_update_kw => {
23847                    let inner = self.parse_one_statement()?;
23848                    let Statement::Update(s) = inner else {
23849                        return Err(
23850                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23851                        );
23852                    };
23853                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23854                }
23855                _ if is_delete_kw => {
23856                    let inner = self.parse_one_statement()?;
23857                    let Statement::Delete(s) = inner else {
23858                        return Err(
23859                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23860                        );
23861                    };
23862                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23863                }
23864                // v7.39 (round 149) — PG 17 allows MERGE as a
23865                // data-modifying CTE body.
23866                _ if is_merge_kw => {
23867                    let inner = self.parse_one_statement()?;
23868                    let Statement::Merge(s) = inner else {
23869                        return Err(
23870                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23871                        );
23872                    };
23873                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23874                }
23875                // v7.39 (round 151) — a CTE body may itself be
23876                // WITH-headed (PG grammar: PreparableStmt carries its
23877                // own with_clause). The nested statement keeps its own
23878                // ctes; the modifying-CTE-at-top-level rule is enforced
23879                // at execution.
23880                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23881                    self.advance(); // WITH
23882                    match self.parse_with_cte_then_select()? {
23883                        Statement::Select(s) => crate::ast::CteBody::Select(s),
23884                        Statement::Insert(s) => {
23885                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23886                        }
23887                        Statement::Update(s) => {
23888                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23889                        }
23890                        Statement::Delete(s) => {
23891                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23892                        }
23893                        Statement::Merge(s) => {
23894                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23895                        }
23896
23897                        other => {
23898                            return Err(self.err(format!(
23899                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23900                            )));
23901                        }
23902                    }
23903                }
23904                other => {
23905                    return Err(self.err(format!(
23906                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23907                    )));
23908                }
23909            };
23910            if !matches!(self.peek(), Token::RParen) {
23911                return Err(self.err(format!(
23912                    "expected ')' after CTE body, got {:?}",
23913                    self.peek()
23914                )));
23915            }
23916            self.advance();
23917            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23918            // CTE, desugared into extra body columns by the engine.
23919            let search = self.parse_cte_search_clause()?;
23920            let cycle = self.parse_cte_cycle_clause()?;
23921            let mut cte = crate::ast::Cte {
23922                name,
23923                body,
23924                recursive,
23925                column_overrides,
23926                search,
23927                cycle,
23928            };
23929            self.validate_recursive_cte(&cte)?;
23930            self.desugar_cte_search_cycle(&mut cte)?;
23931            ctes.push(cte);
23932            if matches!(self.peek(), Token::Comma) {
23933                self.advance();
23934                continue;
23935            }
23936            break;
23937        }
23938        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23939        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23940        // the parsed CTEs to whichever statement the body produces.
23941        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23942        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23943        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23944        match self.peek() {
23945            Token::Select => {
23946                let body_stmt = self.parse_select_stmt()?;
23947                let Statement::Select(mut body) = body_stmt else {
23948                    unreachable!()
23949                };
23950                body.ctes = ctes;
23951                Ok(Statement::Select(body))
23952            }
23953            Token::Insert => {
23954                let body_stmt = self.parse_one_statement()?;
23955                let Statement::Insert(mut body) = body_stmt else {
23956                    unreachable!()
23957                };
23958                body.ctes = ctes;
23959                Ok(Statement::Insert(body))
23960            }
23961            _ if outer_is_update => {
23962                let body_stmt = self.parse_one_statement()?;
23963                let Statement::Update(mut body) = body_stmt else {
23964                    return Err(self.err(format!("expected UPDATE after WITH clause")));
23965                };
23966                body.ctes = ctes;
23967                Ok(Statement::Update(body))
23968            }
23969            _ if outer_is_delete => {
23970                let body_stmt = self.parse_one_statement()?;
23971                let Statement::Delete(mut body) = body_stmt else {
23972                    return Err(self.err(format!("expected DELETE after WITH clause")));
23973                };
23974                body.ctes = ctes;
23975                Ok(Statement::Delete(body))
23976            }
23977            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23978            // WITH RECURSIVE is rejected with PG's exact message
23979            // (parse analysis, transformWithClause).
23980            _ if outer_is_merge => {
23981                if recursive {
23982                    return Err(self.err(String::from(
23983                        "WITH RECURSIVE is not supported for MERGE statement",
23984                    )));
23985                }
23986                let body_stmt = self.parse_one_statement()?;
23987                let Statement::Merge(mut body) = body_stmt else {
23988                    return Err(self.err(format!("expected MERGE after WITH clause")));
23989                };
23990                body.ctes = ctes;
23991                Ok(Statement::Merge(body))
23992            }
23993            other => Err(self.err(format!(
23994                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23995            ))),
23996        }
23997    }
23998
23999    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24000    /// already consumed the leading `EXISTS` ident via
24001    /// `self.advance()`.
24002    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24003    /// the leading `CASE` ident has been consumed (mailrs round-5
24004    /// G9). Supports both the searched form
24005    /// (`CASE WHEN cond THEN val …`) and the simple form
24006    /// (`CASE operand WHEN val THEN val …`).
24007    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24008        // Disambiguate searched vs simple form: if the next token
24009        // is `WHEN`, we're in the searched form. Otherwise the
24010        // intervening expression is the operand.
24011        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24012            None
24013        } else {
24014            Some(Box::new(self.parse_expr(0)?))
24015        };
24016        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24017        loop {
24018            match self.peek() {
24019                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24020                    self.advance();
24021                    let cond = self.parse_expr(0)?;
24022                    match self.peek() {
24023                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24024                            self.advance();
24025                        }
24026                        other => {
24027                            return Err(self.err(alloc::format!(
24028                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24029                            )));
24030                        }
24031                    }
24032                    let value = self.parse_expr(0)?;
24033                    branches.push((cond, value));
24034                }
24035                _ => break,
24036            }
24037        }
24038        if branches.is_empty() {
24039            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24040        }
24041        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24042        {
24043            self.advance();
24044            Some(Box::new(self.parse_expr(0)?))
24045        } else {
24046            None
24047        };
24048        match self.peek() {
24049            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24050                self.advance();
24051            }
24052            other => {
24053                return Err(self.err(alloc::format!(
24054                    "expected END to close CASE expression, got {other:?}"
24055                )));
24056            }
24057        }
24058        Ok(Expr::Case {
24059            operand,
24060            branches,
24061            else_branch,
24062        })
24063    }
24064
24065    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24066    /// query-source position (EXISTS / IN / INSERT source / CTE body /
24067    /// view body). Caller consumed the WITH keyword. Only a SELECT
24068    /// outer is grammatical here; the data-modifying-CTE-at-top-level
24069    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24070    /// maps correctly.
24071    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24072        let inner = self.parse_with_cte_then_select()?;
24073        match inner {
24074            Statement::Select(s) => Ok(s),
24075            other => Err(self.err(format!(
24076                "expected SELECT after WITH in a subquery, got {other:?}"
24077            ))),
24078        }
24079    }
24080
24081    /// True when the next token is the (unquoted) WITH keyword. WITH is
24082    /// reserved in PG, so a bare `with` can never be a column reference
24083    /// in these positions; a quoted `"with"` stays an identifier.
24084    fn peek_is_with_kw(&self) -> bool {
24085        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24086    }
24087
24088    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24089    /// `#[inline(never)]` keeps the large SelectStatement temporaries
24090    /// off parse_expr's recursive frame (the nesting-budget stack
24091    /// cliff — see the round-153 gate regression).
24092    #[inline(never)]
24093    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24094        if self.peek_is_with_kw() {
24095            self.advance();
24096            self.parse_nested_with_select()
24097        } else {
24098            match self.parse_select_stmt()? {
24099                Statement::Select(s) => Ok(s),
24100                other => Err(self.err(alloc::format!(
24101                    "expected SELECT inside ANY/ALL, got {other:?}"
24102                ))),
24103            }
24104        }
24105    }
24106
24107    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24108        if !matches!(self.peek(), Token::LParen) {
24109            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24110        }
24111        self.advance();
24112        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24113        let s = if self.peek_is_with_kw() {
24114            self.advance();
24115            self.parse_nested_with_select()?
24116        } else {
24117            let inner = self.parse_select_stmt()?;
24118            let Statement::Select(s) = inner else {
24119                unreachable!("parse_select_stmt returns Select")
24120            };
24121            s
24122        };
24123        if !matches!(self.peek(), Token::RParen) {
24124            return Err(self.err(format!(
24125                "expected ')' after EXISTS-subquery, got {:?}",
24126                self.peek()
24127            )));
24128        }
24129        self.advance();
24130        Ok(Expr::Exists {
24131            subquery: Box::new(s),
24132            negated,
24133        })
24134    }
24135
24136    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24137        self.advance(); // IN
24138        if !matches!(self.peek(), Token::LParen) {
24139            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24140        }
24141        self.advance();
24142        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24143        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24144        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24145            let s = if self.peek_is_with_kw() {
24146                self.advance();
24147                self.parse_nested_with_select()?
24148            } else {
24149                let inner = self.parse_select_stmt()?;
24150                let Statement::Select(s) = inner else {
24151                    unreachable!("parse_select_stmt always returns Statement::Select")
24152                };
24153                s
24154            };
24155            if !matches!(self.peek(), Token::RParen) {
24156                return Err(self.err(format!(
24157                    "expected ')' after IN-subquery, got {:?}",
24158                    self.peek()
24159                )));
24160            }
24161            self.advance();
24162            return Ok(Expr::InSubquery {
24163                expr: Box::new(expr),
24164                subquery: Box::new(s),
24165                negated,
24166            });
24167        }
24168        let mut elements = Vec::new();
24169        if !matches!(self.peek(), Token::RParen) {
24170            loop {
24171                elements.push(self.parse_expr(0)?);
24172                match self.peek() {
24173                    Token::Comma => {
24174                        self.advance();
24175                    }
24176                    Token::RParen => break,
24177                    other => {
24178                        return Err(
24179                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24180                        );
24181                    }
24182                }
24183            }
24184        }
24185        self.advance(); // ')'
24186        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24187        // left-deep OR-Eq chain: chain depth scaled with the element
24188        // count and overflowed the stack (eval + drop are recursive).
24189        if elements.is_empty() {
24190            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24191        }
24192        Ok(Expr::InList {
24193            expr: Box::new(expr),
24194            list: elements,
24195            negated,
24196        })
24197    }
24198
24199    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24200    /// already consumed by the caller. Elements must be numeric literals
24201    /// (with optional unary `-`); any compound expression is rejected at
24202    /// parse time so the runtime never needs to evaluate inside a vector.
24203    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24204    /// has already consumed the `EXTRACT` token before calling us —
24205    /// we pick up at the opening `(`.
24206    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24207    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24208    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24209    /// per-column OR-fold of
24210    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24211    /// term)` so the existing FTS evaluator handles semantics.
24212    ///
24213    /// The mode modifier is accepted-and-ignored at v7.17 — all
24214    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24215    /// mode operators (`+foo -bar`) would need their own parser
24216    /// (Phase 2.2c); customers who hit them today already get a
24217    /// correct lexeme-match against the bare term, only without
24218    /// the +/- precedence the customer asked for.
24219    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24220        // Already at `MATCH`-consumed position; the dispatcher
24221        // confirmed the next token is `(`.
24222        if !matches!(self.peek(), Token::LParen) {
24223            return Err(self.err(alloc::format!(
24224                "expected '(' after MATCH, got {:?}",
24225                self.peek()
24226            )));
24227        }
24228        self.advance();
24229        let mut cols: Vec<Expr> = Vec::new();
24230        loop {
24231            cols.push(self.parse_expr(0)?);
24232            match self.peek() {
24233                Token::Comma => {
24234                    self.advance();
24235                }
24236                Token::RParen => break,
24237                other => {
24238                    return Err(self.err(alloc::format!(
24239                        "expected ',' or ')' in MATCH column list, got {other:?}"
24240                    )));
24241                }
24242            }
24243        }
24244        self.advance(); // ')'
24245        // Expect AGAINST.
24246        match self.peek() {
24247            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24248                self.advance();
24249            }
24250            other => {
24251                return Err(self.err(alloc::format!(
24252                    "expected AGAINST after MATCH column list, got {other:?}"
24253                )));
24254            }
24255        }
24256        if !matches!(self.peek(), Token::LParen) {
24257            return Err(self.err(alloc::format!(
24258                "expected '(' after AGAINST, got {:?}",
24259                self.peek()
24260            )));
24261        }
24262        self.advance();
24263        // Read AGAINST's argument as a single primary token —
24264        // string literal, placeholder, or column-ref ident. We
24265        // can't call `parse_expr` / `parse_unary` here because
24266        // the postfix chain inside `parse_atom` would greedily
24267        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24268        // and fail at "expected '(' after IN". Customers always
24269        // write a literal or bound parameter in AGAINST, so this
24270        // restriction is non-blocking; the error path explains
24271        // the limit if a more complex expression shows up.
24272        let term = match self.advance() {
24273            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24274            Token::Placeholder(n) => Expr::Placeholder(n),
24275            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24276                qualifier: None,
24277                name: s,
24278            }),
24279            other => {
24280                return Err(self.err(alloc::format!(
24281                    "MATCH ... AGAINST(<term>) expects a string literal, \
24282                     bound parameter, or column ref, got {other:?}"
24283                )));
24284            }
24285        };
24286        // Optional mode tail — accept-and-ignore at v7.17:
24287        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24288        //   IN BOOLEAN MODE
24289        //   WITH QUERY EXPANSION
24290        loop {
24291            match self.peek() {
24292                // IN lexes as a reserved Token::In, not an ident,
24293                // so it gets its own arm.
24294                Token::In => {
24295                    self.advance();
24296                }
24297                Token::Ident(s) | Token::QuotedIdent(s)
24298                    if s.eq_ignore_ascii_case("natural")
24299                        || s.eq_ignore_ascii_case("language")
24300                        || s.eq_ignore_ascii_case("boolean")
24301                        || s.eq_ignore_ascii_case("mode")
24302                        || s.eq_ignore_ascii_case("with")
24303                        || s.eq_ignore_ascii_case("query")
24304                        || s.eq_ignore_ascii_case("expansion") =>
24305                {
24306                    self.advance();
24307                }
24308                _ => break,
24309            }
24310        }
24311        if !matches!(self.peek(), Token::RParen) {
24312            return Err(self.err(alloc::format!(
24313                "expected ')' to close AGAINST, got {:?}",
24314                self.peek()
24315            )));
24316        }
24317        self.advance();
24318        // Build per-column `to_tsvector('simple', col) @@
24319        // plainto_tsquery('simple', term)` and OR-fold.
24320        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24321        let plainto = Expr::FunctionCall {
24322            name: String::from("plainto_tsquery"),
24323            args: alloc::vec![simple_lit(), term.clone()],
24324        };
24325        let mut folded: Option<Expr> = None;
24326        for col in cols {
24327            let to_tsv = Expr::FunctionCall {
24328                name: String::from("to_tsvector"),
24329                args: alloc::vec![simple_lit(), col],
24330            };
24331            let leaf = Expr::Binary {
24332                lhs: Box::new(to_tsv),
24333                op: crate::ast::BinOp::TsMatch,
24334                rhs: Box::new(plainto.clone()),
24335            };
24336            folded = Some(match folded {
24337                None => leaf,
24338                Some(prev) => Expr::Binary {
24339                    lhs: Box::new(prev),
24340                    op: crate::ast::BinOp::Or,
24341                    rhs: Box::new(leaf),
24342                },
24343            });
24344        }
24345        match folded {
24346            Some(e) => Ok(e),
24347            None => Err(self.err(String::from(
24348                "MATCH(...) AGAINST(...) requires at least one column",
24349            ))),
24350        }
24351    }
24352
24353    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24354        if !matches!(self.peek(), Token::LParen) {
24355            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24356        }
24357        self.advance();
24358        let field_name = self.expect_ident_like()?;
24359        let field = match field_name.to_ascii_lowercase().as_str() {
24360            // PG accepts the plural spellings (years/months/…/millenniums) as
24361            // aliases for the singular fields — its datetime unit table has both.
24362            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24363            "year" | "years" => ExtractField::Year,
24364            "month" | "months" => ExtractField::Month,
24365            "day" | "days" => ExtractField::Day,
24366            "hour" | "hours" => ExtractField::Hour,
24367            "minute" | "minutes" => ExtractField::Minute,
24368            "second" | "seconds" => ExtractField::Second,
24369            "microsecond" | "microseconds" => ExtractField::Microsecond,
24370            "epoch" => ExtractField::Epoch,
24371            "dow" => ExtractField::Dow,
24372            "isodow" => ExtractField::Isodow,
24373            "doy" => ExtractField::Doy,
24374            "week" | "weeks" => ExtractField::Week,
24375            "isoyear" => ExtractField::Isoyear,
24376            "quarter" => ExtractField::Quarter,
24377            "decade" | "decades" => ExtractField::Decade,
24378            "century" | "centuries" => ExtractField::Century,
24379            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24380            "julian" => ExtractField::Julian,
24381            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24382            "timezone" => ExtractField::Timezone,
24383            "timezone_hour" => ExtractField::TimezoneHour,
24384            "timezone_minute" => ExtractField::TimezoneMinute,
24385            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24386            // reports an unknown one with the source type (22023); carry the
24387            // raw name so eval can word it.
24388            other => ExtractField::Other(alloc::string::String::from(other)),
24389        };
24390        if !matches!(self.peek(), Token::From) {
24391            return Err(self.err(format!(
24392                "expected FROM after EXTRACT field, got {:?}",
24393                self.peek()
24394            )));
24395        }
24396        self.advance();
24397        let source = self.parse_expr(0)?;
24398        if !matches!(self.peek(), Token::RParen) {
24399            return Err(self.err(format!(
24400                "expected ')' to close EXTRACT, got {:?}",
24401                self.peek()
24402            )));
24403        }
24404        self.advance();
24405        Ok(Expr::Extract {
24406            field,
24407            source: Box::new(source),
24408        })
24409    }
24410
24411    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24412    /// is already consumed; we expect a single string literal next and
24413    /// resolve it into `Literal::Interval` at parse time so the engine
24414    /// never has to re-tokenise inside the string.
24415    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24416    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24417    /// is the SQL-standard form and is left to the path below.
24418    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24419        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24420        let (offset, sign) = match self.peek() {
24421            Token::Minus => (1, "-"),
24422            _ => (0, ""),
24423        };
24424        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24425            return None;
24426        };
24427        self.tokens
24428            .get(self.pos + offset + 1)
24429            .filter(|t| mysql_interval_unit(t).is_some())?;
24430        Some((alloc::format!("{sign}{n}"), offset + 1))
24431    }
24432
24433    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24434    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24435    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24436    ///
24437    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24438    /// this by parsing the group and then restoring `self.pos` — which could
24439    /// never have worked, because `advance()` DESTROYS the token it returns
24440    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24441    /// inert only because both branches errored back then.
24442    fn interval_paren_is_quantity(&self) -> bool {
24443        let mut depth = 0usize;
24444        let mut saw_top_level_comma = false;
24445        let mut i = self.pos;
24446        while let Some(tok) = self.tokens.get(i) {
24447            match tok {
24448                Token::LParen => depth += 1,
24449                Token::RParen => {
24450                    depth = depth.saturating_sub(1);
24451                    if depth == 0 {
24452                        return !saw_top_level_comma
24453                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24454                                .is_some();
24455                    }
24456                }
24457                // A comma directly inside the outermost parens means the
24458                // argument list of the INTERVAL() function.
24459                Token::Comma if depth == 1 => saw_top_level_comma = true,
24460                Token::Eof => return false,
24461                _ => {}
24462            }
24463            i += 1;
24464        }
24465        false
24466    }
24467
24468    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24469        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24470        // (the index of the last Ni ≤ N), distinct from the interval literal.
24471        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24472        // is decided by a non-destructive lookahead (round 422) before either
24473        // branch consumes anything. MySQL only.
24474        if self.mysql_dialect
24475            && matches!(self.peek(), Token::LParen)
24476            && !self.interval_paren_is_quantity()
24477        {
24478            self.advance(); // (
24479            let mut args = Vec::new();
24480            if !matches!(self.peek(), Token::RParen) {
24481                loop {
24482                    args.push(self.parse_expr(0)?);
24483                    if matches!(self.peek(), Token::Comma) {
24484                        self.advance();
24485                        continue;
24486                    }
24487                    break;
24488                }
24489            }
24490            if !matches!(self.peek(), Token::RParen) {
24491                return Err(self.err(alloc::format!(
24492                    "expected ')' after INTERVAL() arguments, got {:?}",
24493                    self.peek()
24494                )));
24495            }
24496            self.advance(); // )
24497            return Ok(Expr::FunctionCall {
24498                name: alloc::string::String::from("interval"),
24499                args,
24500            });
24501        }
24502        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24503        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24504        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24505        // writes every date arithmetic there is, and it did not parse at
24506        // all. PG rejects the unquoted form outright (`syntax error at or
24507        // near "1"`, measured), so it is taken only in the MySQL dialect —
24508        // PG's own `INTERVAL '1' DAY` is untouched below.
24509        if self.mysql_dialect
24510            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24511        {
24512            for _ in 0..consume {
24513                self.advance(); // the optional `-` and the number
24514            }
24515            let Some(unit) = mysql_interval_unit(self.peek()) else {
24516                return Err(self.err(alloc::format!(
24517                    "expected an interval unit after INTERVAL {text}, got {:?}",
24518                    self.peek()
24519                )));
24520            };
24521            self.advance(); // the unit
24522            let (months, days, micros) = scale_mysql_interval(&text, unit)
24523                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24524            return Ok(Expr::Literal(Literal::Interval {
24525                months,
24526                days,
24527                micros,
24528                // The canonical rendering, so Display round-trips into a
24529                // form both dialects read back.
24530                text: alloc::format!("{text} {unit}"),
24531            }));
24532        }
24533        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24534        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24535        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24536        // Those cannot fold into a compile-time `Literal::Interval`, so they
24537        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24538        // builtin, which builds the value at run time (and yields NULL for a
24539        // NULL quantity, as MariaDB does). The literal path above still folds
24540        // the constant case — it is cheaper and round-trips through Display.
24541        //
24542        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24543        // MySQL's quoted spelling) keep the qualifier path below.
24544        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24545            let qty = self.parse_expr(0)?;
24546            let Some(unit) = mysql_interval_unit(self.peek()) else {
24547                return Err(self.err(alloc::format!(
24548                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24549                    self.peek()
24550                )));
24551            };
24552            self.advance(); // the unit
24553            return Ok(make_interval_call(qty, unit));
24554        }
24555        let tok = self.advance();
24556        let Token::String(text) = tok else {
24557            return Err(self.err(format!(
24558                "expected string literal after INTERVAL, got {tok:?}"
24559            )));
24560        };
24561        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24562        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24563        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24564        // bare number means and the leading/trailing precision.
24565        let field1 = interval_field_of(self.peek());
24566        let qualifier = if let Some(f1) = field1 {
24567            self.advance();
24568            let f2 = if matches!(self.peek(), Token::To) {
24569                self.advance();
24570                let Some(f) = interval_field_of(self.peek()) else {
24571                    return Err(self.err(format!(
24572                        "expected an interval field after TO, got {:?}",
24573                        self.peek()
24574                    )));
24575                };
24576                self.advance();
24577                Some(f)
24578            } else {
24579                None
24580            };
24581            Some((f1, f2))
24582        } else {
24583            None
24584        };
24585        let (months, days, micros) = match qualifier {
24586            Some(q) => interpret_qualified_interval(&text, q),
24587            None => parse_interval_text(&text),
24588        }
24589        .ok_or_else(|| ParseError {
24590            message: format!(
24591                "cannot parse INTERVAL {text:?}; \
24592                     expected `<n> <unit> [<n> <unit> ...]` with units \
24593                     microsecond[s], millisecond[s], second[s], minute[s], \
24594                     hour[s], day[s], week[s], month[s], year[s]"
24595            ),
24596            token_pos: self.consumed_pos(),
24597        })?;
24598        Ok(Expr::Literal(Literal::Interval {
24599            months,
24600            days,
24601            micros,
24602            text,
24603        }))
24604    }
24605
24606    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24607    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24608    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24609    /// than a pgvector literal.
24610    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24611        self.advance(); // consume `[`
24612        let mut items: Vec<Expr> = Vec::new();
24613        if !matches!(self.peek(), Token::RBracket) {
24614            loop {
24615                if matches!(self.peek(), Token::LBracket) {
24616                    items.push(self.parse_array_bracket_body()?);
24617                } else {
24618                    items.push(self.parse_expr(0)?);
24619                }
24620                match self.peek() {
24621                    Token::Comma => {
24622                        self.advance();
24623                    }
24624                    Token::RBracket => break,
24625                    other => {
24626                        return Err(self.err(alloc::format!(
24627                            "expected ',' or ']' in array literal, got {other:?}"
24628                        )));
24629                    }
24630                }
24631            }
24632        }
24633        self.advance(); // consume `]`
24634        Ok(Expr::Array(items))
24635    }
24636
24637    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24638        let mut elems = Vec::new();
24639        if matches!(self.peek(), Token::RBracket) {
24640            self.advance();
24641            return Ok(Expr::Literal(Literal::Vector(elems)));
24642        }
24643        loop {
24644            let e = self.parse_expr(0)?;
24645            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24646                message: format!("vector element must be a numeric literal, got {e:?}"),
24647                token_pos: self.pos,
24648            })?;
24649            elems.push(x);
24650            match self.peek() {
24651                Token::Comma => {
24652                    self.advance();
24653                }
24654                Token::RBracket => {
24655                    self.advance();
24656                    break;
24657                }
24658                other => {
24659                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24660                }
24661            }
24662        }
24663        Ok(Expr::Literal(Literal::Vector(elems)))
24664    }
24665
24666    /// Atom that started with an identifier: could be `t.col`, `col`, or
24667    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24668    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24669    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24670    /// is optional; an empty `()` is also legal (PG semantics).
24671    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24672    /// modifier between `name(args)` and `OVER (...)`. Default is
24673    /// `Respect`. Unrecognised idents leave the stream unchanged.
24674    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24675        let Token::Ident(s) = self.peek().clone() else {
24676            return NullTreatment::Respect;
24677        };
24678        let is_ignore = s.eq_ignore_ascii_case("ignore");
24679        let is_respect = s.eq_ignore_ascii_case("respect");
24680        if !is_ignore && !is_respect {
24681            return NullTreatment::Respect;
24682        }
24683        // Lookahead for NULLS — only consume both tokens together.
24684        // pos+1 must hold a "nulls" ident.
24685        if self.pos + 1 < self.tokens.len()
24686            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24687            && s2.eq_ignore_ascii_case("nulls")
24688        {
24689            self.advance();
24690            self.advance();
24691            return if is_ignore {
24692                NullTreatment::Ignore
24693            } else {
24694                NullTreatment::Respect
24695            };
24696        }
24697        NullTreatment::Respect
24698    }
24699
24700    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24701    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24702    /// (same shape as the `OVER` tail). Consumes the whole clause and
24703    /// returns the predicate; returns `None` when no `FILTER` follows.
24704    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24705        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24706            return Ok(None);
24707        };
24708        if !s.eq_ignore_ascii_case("filter") {
24709            return Ok(None);
24710        }
24711        self.advance(); // FILTER
24712        if !matches!(self.peek(), Token::LParen) {
24713            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24714        }
24715        self.advance(); // (
24716        if !matches!(self.peek(), Token::Where) {
24717            return Err(self.err(format!(
24718                "expected WHERE inside FILTER (...), got {:?}",
24719                self.peek()
24720            )));
24721        }
24722        self.advance(); // WHERE
24723        let cond = self.parse_expr(0)?;
24724        if !matches!(self.peek(), Token::RParen) {
24725            return Err(self.err(format!(
24726                "expected ')' to close FILTER (WHERE ...), got {:?}",
24727                self.peek()
24728            )));
24729        }
24730        self.advance(); // )
24731        Ok(Some(Box::new(cond)))
24732    }
24733
24734    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24735    /// the separator as the aggregate's second argument, which is the
24736    /// shape `string_agg` already takes. Returns whether one was there.
24737    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24738        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24739            return Ok(false);
24740        }
24741        self.advance();
24742        let Token::String(sep) = self.peek().clone() else {
24743            return Err(self.err(alloc::format!(
24744                "expected a string literal after SEPARATOR, got {:?}",
24745                self.peek()
24746            )));
24747        };
24748        self.advance();
24749        args.push(Expr::Literal(Literal::String(sep)));
24750        Ok(true)
24751    }
24752
24753    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24754    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24755    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24756    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24757    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24758        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24759            return Ok(Vec::new());
24760        };
24761        if !s.eq_ignore_ascii_case("within") {
24762            return Ok(Vec::new());
24763        }
24764        self.advance(); // WITHIN
24765        if !matches!(self.peek(), Token::Group) {
24766            return Err(self.err(format!(
24767                "expected GROUP after WITHIN, got {:?}",
24768                self.peek()
24769            )));
24770        }
24771        self.advance(); // GROUP
24772        if !matches!(self.peek(), Token::LParen) {
24773            return Err(self.err(format!(
24774                "expected '(' after WITHIN GROUP, got {:?}",
24775                self.peek()
24776            )));
24777        }
24778        self.advance(); // (
24779        if !matches!(self.peek(), Token::Order) {
24780            return Err(self.err(format!(
24781                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24782                self.peek()
24783            )));
24784        }
24785        self.advance(); // ORDER
24786        if !self.peek_is_by() {
24787            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24788        }
24789        self.advance(); // BY
24790        let mut keys: Vec<OrderBy> = Vec::new();
24791        loop {
24792            // v7.39 (round 691) — save/restore, the discipline this parser
24793            // already uses around `pending_sample_preds`, so a subquery inside
24794            // a key neither inherits nor leaks the channel.
24795            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24796            let saved_coll = self.order_key_collation.take();
24797            let parsed = self.parse_expr(0);
24798            self.in_order_by_key = saved_flag;
24799            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24800            let expr = parsed?;
24801            let desc = if matches!(self.peek(), Token::Desc) {
24802                self.advance();
24803                true
24804            } else if matches!(self.peek(), Token::Asc) {
24805                self.advance();
24806                false
24807            } else {
24808                false
24809            };
24810            let nulls_first = self.parse_optional_nulls_placement()?;
24811            keys.push(OrderBy {
24812                expr,
24813                desc,
24814                nulls_first,
24815                collation,
24816            });
24817            if matches!(self.peek(), Token::Comma) {
24818                self.advance();
24819            } else {
24820                break;
24821            }
24822        }
24823        if !matches!(self.peek(), Token::RParen) {
24824            return Err(self.err(format!(
24825                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24826                self.peek()
24827            )));
24828        }
24829        self.advance(); // )
24830        Ok(keys)
24831    }
24832
24833    /// No frame clause is supported.
24834    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24835    fn parse_over_clause(
24836        &mut self,
24837    ) -> Result<
24838        (
24839            Vec<Expr>,
24840            Vec<(Expr, bool, Option<bool>)>,
24841            Option<WindowFrame>,
24842        ),
24843        ParseError,
24844    > {
24845        // `OVER w` — a named-window reference. The WINDOW clause
24846        // parses after the select list, so the name rides out as a
24847        // marker in partition_by; parse_bare_select substitutes the
24848        // definition once the clause is known.
24849        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24850            let name = w.clone();
24851            self.advance();
24852            return Ok((
24853                alloc::vec![Expr::Column(crate::ast::ColumnName {
24854                    qualifier: Some("__named_window__".to_string()),
24855                    name,
24856                })],
24857                Vec::new(),
24858                None,
24859            ));
24860        }
24861        if !matches!(self.peek(), Token::LParen) {
24862            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24863        }
24864        self.advance();
24865        let mut partition_by = Vec::new();
24866        let mut order_by = Vec::new();
24867        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24868        // window, refined in place. PG's rules (probed against 18.4) differ
24869        // from the bare `OVER w1` form, so the reference rides out under its
24870        // own marker and `substitute_named_windows` applies them. The base
24871        // name is any leading identifier that isn't a window-spec keyword.
24872        let base_window = match self.peek() {
24873            Token::Ident(s) | Token::QuotedIdent(s)
24874                if !s.eq_ignore_ascii_case("partition")
24875                    && !s.eq_ignore_ascii_case("rows")
24876                    && !s.eq_ignore_ascii_case("range")
24877                    && !s.eq_ignore_ascii_case("groups") =>
24878            {
24879                let n = s.clone();
24880                self.advance();
24881                Some(n)
24882            }
24883            _ => None,
24884        };
24885        // PARTITION BY ?
24886        // v7.37.6-B promoted PARTITION to a reserved keyword
24887        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24888        // `Token::Ident("partition")`. Accept both so older sources
24889        // and the new lexer surface land on the same path.
24890        let is_partition_kw = match self.peek() {
24891            Token::Partition => true,
24892            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24893            _ => false,
24894        };
24895        if is_partition_kw {
24896            self.advance();
24897            if !self.peek_is_by() {
24898                return Err(self.err(format!(
24899                    "expected BY after PARTITION, got {:?}",
24900                    self.peek()
24901                )));
24902            }
24903            self.advance();
24904            loop {
24905                partition_by.push(self.parse_expr(0)?);
24906                if matches!(self.peek(), Token::Comma) {
24907                    self.advance();
24908                    continue;
24909                }
24910                break;
24911            }
24912        }
24913        // ORDER BY ?
24914        if matches!(self.peek(), Token::Order) {
24915            self.advance();
24916            if !self.peek_is_by() {
24917                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24918            }
24919            self.advance();
24920            loop {
24921                let e = self.parse_expr(0)?;
24922                let desc = if matches!(self.peek(), Token::Desc) {
24923                    self.advance();
24924                    true
24925                } else if matches!(self.peek(), Token::Asc) {
24926                    self.advance();
24927                    false
24928                } else {
24929                    false
24930                };
24931                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24932                let nulls_first = self.parse_optional_nulls_placement()?;
24933                order_by.push((e, desc, nulls_first));
24934                if matches!(self.peek(), Token::Comma) {
24935                    self.advance();
24936                    continue;
24937                }
24938                break;
24939            }
24940        }
24941        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24942        // Both keywords come through the lexer as identifiers; match
24943        // case-insensitively.
24944        let mut frame: Option<WindowFrame> = None;
24945        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24946            let kind = if s.eq_ignore_ascii_case("rows") {
24947                Some(FrameKind::Rows)
24948            } else if s.eq_ignore_ascii_case("range") {
24949                Some(FrameKind::Range)
24950            } else if s.eq_ignore_ascii_case("groups") {
24951                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24952                Some(FrameKind::Groups)
24953            } else {
24954                None
24955            };
24956            if let Some(kind) = kind {
24957                self.advance();
24958                frame = Some(self.parse_frame_tail(kind)?);
24959            }
24960        }
24961        if !matches!(self.peek(), Token::RParen) {
24962            return Err(self.err(format!(
24963                "expected ')' to close OVER clause, got {:?}",
24964                self.peek()
24965            )));
24966        }
24967        self.advance();
24968        if let Some(base) = base_window {
24969            // A copy may refine but never override the base's partitioning
24970            // (PG rejects it outright, before looking the name up).
24971            if !partition_by.is_empty() {
24972                return Err(self.err(alloc::format!(
24973                    "cannot override PARTITION BY clause of window \"{base}\""
24974                )));
24975            }
24976            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24977                qualifier: Some("__named_window_ref__".to_string()),
24978                name: base,
24979            })];
24980        }
24981        Ok((partition_by, order_by, frame))
24982    }
24983
24984    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24985    /// or `RANGE` keyword was just consumed. Accepts both
24986    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24987    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24988    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24989    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24990        let (start, end) = if matches!(self.peek(), Token::Between) {
24991            self.advance();
24992            let start = self.parse_frame_bound()?;
24993            if !matches!(self.peek(), Token::And) {
24994                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24995            }
24996            self.advance();
24997            let end = self.parse_frame_bound()?;
24998            (start, Some(end))
24999        } else {
25000            (self.parse_frame_bound()?, None)
25001        };
25002        let exclude = self.parse_frame_exclusion()?;
25003        Ok(WindowFrame {
25004            kind,
25005            start,
25006            end,
25007            exclude,
25008        })
25009    }
25010
25011    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25012    /// after a frame spec. NO OTHERS is the default no-op.
25013    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25014        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25015            return Ok(FrameExclusion::NoOthers);
25016        }
25017        self.advance(); // EXCLUDE
25018        match self.peek() {
25019            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25020                self.advance();
25021                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25022                    return Err(self.err(format!(
25023                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25024                        self.peek()
25025                    )));
25026                }
25027                self.advance();
25028                Ok(FrameExclusion::CurrentRow)
25029            }
25030            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25031            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25032            // Without this arm it fell to the catch-all, whose message
25033            // self-contradictingly listed GROUP as expected.
25034            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25035                self.advance();
25036                Ok(FrameExclusion::Group)
25037            }
25038            Token::Group => {
25039                self.advance();
25040                Ok(FrameExclusion::Group)
25041            }
25042            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25043                self.advance();
25044                Ok(FrameExclusion::Ties)
25045            }
25046            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25047                self.advance();
25048                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25049                    return Err(self.err(format!(
25050                        "expected OTHERS after EXCLUDE NO, got {:?}",
25051                        self.peek()
25052                    )));
25053                }
25054                self.advance();
25055                Ok(FrameExclusion::NoOthers)
25056            }
25057            other => Err(self.err(format!(
25058                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25059            ))),
25060        }
25061    }
25062
25063    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25064    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25065    /// `UNBOUNDED FOLLOWING`.
25066    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25067        // Interval-typed offset for a value-based RANGE frame over a
25068        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25069        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25070        // PRECEDING`.
25071        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25072            let dir = self.expect_ident_like()?;
25073            return if dir.eq_ignore_ascii_case("preceding") {
25074                Ok(FrameBound::IntervalPreceding {
25075                    months,
25076                    days,
25077                    micros,
25078                })
25079            } else if dir.eq_ignore_ascii_case("following") {
25080                Ok(FrameBound::IntervalFollowing {
25081                    months,
25082                    days,
25083                    micros,
25084                })
25085            } else {
25086                Err(self.err(format!(
25087                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25088                )))
25089            };
25090        }
25091        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25092        if let Token::Integer(n) = *self.peek() {
25093            self.advance();
25094            let n: u64 = u64::try_from(n).map_err(|_| {
25095                self.err(format!(
25096                    "invalid frame offset {n} — expected non-negative integer"
25097                ))
25098            })?;
25099            let dir = self.expect_ident_like()?;
25100            return if dir.eq_ignore_ascii_case("preceding") {
25101                Ok(FrameBound::OffsetPreceding(n))
25102            } else if dir.eq_ignore_ascii_case("following") {
25103                Ok(FrameBound::OffsetFollowing(n))
25104            } else {
25105                Err(self.err(format!(
25106                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25107                )))
25108            };
25109        }
25110        let first = self.expect_ident_like()?;
25111        if first.eq_ignore_ascii_case("unbounded") {
25112            let dir = self.expect_ident_like()?;
25113            return if dir.eq_ignore_ascii_case("preceding") {
25114                Ok(FrameBound::UnboundedPreceding)
25115            } else if dir.eq_ignore_ascii_case("following") {
25116                Ok(FrameBound::UnboundedFollowing)
25117            } else {
25118                Err(self.err(format!(
25119                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25120                )))
25121            };
25122        }
25123        if first.eq_ignore_ascii_case("current") {
25124            let row = self.expect_ident_like()?;
25125            if !row.eq_ignore_ascii_case("row") {
25126                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25127            }
25128            return Ok(FrameBound::CurrentRow);
25129        }
25130        Err(self.err(format!(
25131            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25132        )))
25133    }
25134
25135    /// Detect and consume a leading interval offset in a frame bound —
25136    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25137    /// `(months, days, micros)`. Leaves the cursor on the trailing
25138    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25139    /// when the next tokens are not an interval offset.
25140    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25141        // Shape A — `INTERVAL '1 day'`.
25142        if matches!(self.peek(), Token::Interval) {
25143            self.advance(); // INTERVAL
25144            let atom = self.parse_interval_atom()?;
25145            if let Expr::Literal(Literal::Interval {
25146                months,
25147                days,
25148                micros,
25149                ..
25150            }) = atom
25151            {
25152                return Ok(Some((months, days, micros)));
25153            }
25154            return Err(self.err("expected an interval literal in frame offset".to_string()));
25155        }
25156        // Shape B — `'1 day'::interval`. Look ahead for the exact
25157        // string / `::` / interval-target triple before committing.
25158        if let Token::String(text) = self.peek() {
25159            let target_is_interval = match self.tokens.get(self.pos + 2) {
25160                Some(Token::Interval) => true,
25161                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25162                _ => false,
25163            };
25164            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25165                && target_is_interval;
25166            if is_cast {
25167                let text = text.clone();
25168                self.advance(); // string
25169                self.advance(); // ::
25170                self.advance(); // interval
25171                let parts = parse_interval_text(&text).ok_or_else(|| {
25172                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25173                })?;
25174                return Ok(Some(parts));
25175            }
25176        }
25177        Ok(None)
25178    }
25179
25180    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25181        if matches!(self.peek(), Token::Dot) {
25182            self.advance();
25183            let name = self.expect_ident_like()?;
25184            // v7.14.0 — schema-qualified function call
25185            // `<schema>.<fn>(args)`. PG dumps emit
25186            // `pg_catalog.set_config(...)` in the preamble. SPG
25187            // is single-namespace: drop the schema prefix and
25188            // route the dispatch on the bare function name.
25189            if matches!(self.peek(), Token::LParen) {
25190                return self.finish_ident_atom(name);
25191            }
25192            return Ok(Expr::Column(ColumnName {
25193                qualifier: Some(first),
25194                name,
25195            }));
25196        }
25197        if matches!(self.peek(), Token::LParen) {
25198            self.advance();
25199            // `COUNT(*)` — special-cased here because `*` isn't a normal
25200            // expression token. Lower-case match on `first` since the lexer
25201            // folds identifiers.
25202            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25203                self.advance();
25204                if !matches!(self.peek(), Token::RParen) {
25205                    return Err(self.err(format!(
25206                        "expected ')' after COUNT(*), got {:?}",
25207                        self.peek()
25208                    )));
25209                }
25210                self.advance();
25211                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25212                let filter = self.parse_filter_clause()?;
25213                // v4.12: COUNT(*) OVER (...) — same window tail.
25214                let null_treatment = self.parse_null_treatment_modifier();
25215                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25216                    && s.eq_ignore_ascii_case("over")
25217                {
25218                    self.advance();
25219                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25220                    return Ok(Expr::WindowFunction {
25221                        name: "count_star".into(),
25222                        args: Vec::new(),
25223                        partition_by,
25224                        order_by,
25225                        frame,
25226                        null_treatment,
25227                        filter,
25228                    });
25229                }
25230                if let Some(filter) = filter {
25231                    return Ok(Expr::AggregateOrdered {
25232                        call: Box::new(Expr::FunctionCall {
25233                            name: "count_star".into(),
25234                            args: Vec::new(),
25235                        }),
25236                        order_by: Vec::new(),
25237                        distinct: false,
25238                        filter: Some(filter),
25239                    });
25240                }
25241                return Ok(Expr::FunctionCall {
25242                    name: "count_star".into(),
25243                    args: Vec::new(),
25244                });
25245            }
25246            // Function call. PG-style: zero-or-more comma-separated args.
25247            let mut args = Vec::new();
25248            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25249            // Names are collected in lock-step with `args` and resolved to
25250            // positional order after the loop (the AST stays positional).
25251            let mut arg_names: Vec<Option<String>> = Vec::new();
25252            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25253            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25254            // seen, so the value arguments before it can be folded.
25255            let mut saw_separator = false;
25256            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25257            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25258            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25259            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25260                self.advance();
25261                true
25262            } else if matches!(self.peek(), Token::All) {
25263                self.advance();
25264                false
25265            } else {
25266                false
25267            };
25268            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25269            // TIMESTAMPDIFF take a bare unit keyword as the first
25270            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25271            // bare type keyword (DATE / TIME / DATETIME); lower them
25272            // onto string literals so the evaluator sees plain text.
25273            if ((first.eq_ignore_ascii_case("timestampadd")
25274                || first.eq_ignore_ascii_case("timestampdiff"))
25275                && matches!(self.peek(), Token::Ident(u) if matches!(
25276                    u.to_ascii_lowercase().as_str(),
25277                    "microsecond" | "second" | "minute" | "hour" | "day"
25278                        | "week" | "month" | "quarter" | "year"
25279                )))
25280                || (first.eq_ignore_ascii_case("get_format")
25281                    && matches!(self.peek(), Token::Ident(u) if matches!(
25282                        u.to_ascii_lowercase().as_str(),
25283                        "date" | "time" | "datetime" | "timestamp"
25284                    )))
25285            {
25286                if let Token::Ident(u) = self.peek() {
25287                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25288                }
25289                self.advance();
25290                if matches!(self.peek(), Token::Comma) {
25291                    self.advance();
25292                }
25293            }
25294            // `ROW(a, b, …)` keyword constructor. Followed by a
25295            // comparison operator or [NOT] IN it joins the paren
25296            // row-constructor machinery (fieldwise parse-time
25297            // expansion); bare, it stays a `row` call the evaluator
25298            // renders as PG record text.
25299            if first.eq_ignore_ascii_case("row") {
25300                let mut row_items = Vec::new();
25301                if !matches!(self.peek(), Token::RParen) {
25302                    loop {
25303                        row_items.push(self.parse_expr(0)?);
25304                        match self.peek() {
25305                            Token::Comma => {
25306                                self.advance();
25307                            }
25308                            Token::RParen => break,
25309                            other => {
25310                                return Err(self.err(format!(
25311                                    "expected ',' or ')' in ROW(...), got {other:?}"
25312                                )));
25313                            }
25314                        }
25315                    }
25316                }
25317                self.advance(); // ')'
25318                let comparison_follows = matches!(
25319                    self.peek(),
25320                    Token::Eq
25321                        | Token::NotEq
25322                        | Token::Lt
25323                        | Token::LtEq
25324                        | Token::Gt
25325                        | Token::GtEq
25326                        | Token::In
25327                ) || (matches!(self.peek(), Token::Not)
25328                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25329                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25330                if comparison_follows && !row_items.is_empty() {
25331                    return self.parse_row_comparison_tail(row_items);
25332                }
25333                return Ok(Expr::FunctionCall {
25334                    name: String::from("row"),
25335                    args: row_items,
25336                });
25337            }
25338            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25339            // the parse-mode keyword introduces the source text. SPG
25340            // carries XML as text, so both modes lower to __xmlparse(expr)
25341            // which validates well-formedness and returns Value::Xml.
25342            if first.eq_ignore_ascii_case("xmlparse")
25343                && matches!(self.peek(), Token::Ident(kw)
25344                    if kw.eq_ignore_ascii_case("document")
25345                        || kw.eq_ignore_ascii_case("content"))
25346            {
25347                let mode = match self.advance() {
25348                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25349                    _ => unreachable!("peeked an ident"),
25350                };
25351                let src = self.parse_expr(0)?;
25352                if !matches!(self.peek(), Token::RParen) {
25353                    return Err(self.err(format!(
25354                        "expected ')' to close XMLPARSE, got {:?}",
25355                        self.peek()
25356                    )));
25357                }
25358                self.advance();
25359                return Ok(Expr::FunctionCall {
25360                    name: String::from("__xmlparse"),
25361                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25362                });
25363            }
25364            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25365            // keyword introduces the element name (a bare or quoted
25366            // identifier), then optional content expressions. Lower to a
25367            // plain `xmlelement(name_text, content …)` call.
25368            if first.eq_ignore_ascii_case("xmlelement")
25369                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25370            {
25371                self.advance(); // consume NAME
25372                let elem_name = match self.peek().clone() {
25373                    Token::Ident(n) | Token::QuotedIdent(n) => {
25374                        self.advance();
25375                        n
25376                    }
25377                    other => {
25378                        return Err(self.err(format!(
25379                            "expected element name after XMLELEMENT NAME, got {other:?}"
25380                        )));
25381                    }
25382                };
25383                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25384                while matches!(self.peek(), Token::Comma) {
25385                    self.advance();
25386                    args.push(self.parse_expr(0)?);
25387                }
25388                if !matches!(self.peek(), Token::RParen) {
25389                    return Err(self.err(format!(
25390                        "expected ')' to close XMLELEMENT, got {:?}",
25391                        self.peek()
25392                    )));
25393                }
25394                self.advance();
25395                return Ok(Expr::FunctionCall {
25396                    name: String::from("xmlelement"),
25397                    args,
25398                });
25399            }
25400            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25401            // becomes a `<name>value</name>` element; a bare column infers its
25402            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25403            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25404                let mut args: Vec<Expr> = Vec::new();
25405                loop {
25406                    let val = self.parse_expr(0)?;
25407                    let name = if matches!(self.peek(), Token::As) {
25408                        self.advance();
25409                        match self.peek().clone() {
25410                            Token::Ident(n) | Token::QuotedIdent(n) => {
25411                                self.advance();
25412                                n
25413                            }
25414                            other => {
25415                                return Err(self.err(format!(
25416                                    "expected name after AS in XMLFOREST, got {other:?}"
25417                                )));
25418                            }
25419                        }
25420                    } else if let Expr::Column(c) = &val {
25421                        c.name.clone()
25422                    } else {
25423                        return Err(
25424                            self.err("XMLFOREST element without a column name needs AS".into())
25425                        );
25426                    };
25427                    args.push(Expr::Literal(Literal::String(name)));
25428                    args.push(val);
25429                    if matches!(self.peek(), Token::Comma) {
25430                        self.advance();
25431                    } else {
25432                        break;
25433                    }
25434                }
25435                if !matches!(self.peek(), Token::RParen) {
25436                    return Err(self.err(format!(
25437                        "expected ')' to close XMLFOREST, got {:?}",
25438                        self.peek()
25439                    )));
25440                }
25441                self.advance();
25442                return Ok(Expr::FunctionCall {
25443                    name: String::from("xmlforest"),
25444                    args,
25445                });
25446            }
25447            // SQL-standard `POSITION(sub IN str)` — lowers onto
25448            // strpos(str, sub). IN is the argument separator here,
25449            // so the needle parses with the IN-tail suppressed.
25450            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25451                let saved = self.suppress_in_tail;
25452                self.suppress_in_tail = true;
25453                let needle = self.parse_expr(0);
25454                self.suppress_in_tail = saved;
25455                let needle = needle?;
25456                if matches!(self.peek(), Token::In) {
25457                    self.advance();
25458                    let haystack = self.parse_expr(0)?;
25459                    if !matches!(self.peek(), Token::RParen) {
25460                        return Err(self.err(format!(
25461                            "expected ')' to close POSITION, got {:?}",
25462                            self.peek()
25463                        )));
25464                    }
25465                    self.advance();
25466                    return Ok(Expr::FunctionCall {
25467                        name: String::from("strpos"),
25468                        args: alloc::vec![haystack, needle],
25469                    });
25470                }
25471                // position(sub, str) comma form (incl. bytea) —
25472                // hand the parsed first arg to the generic list.
25473                args.push(needle);
25474                if matches!(self.peek(), Token::Comma) {
25475                    self.advance();
25476                }
25477            }
25478            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25479            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25480            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25481            // riding the generic argument list below.
25482            if first.eq_ignore_ascii_case("trim") {
25483                let mode = match self.peek() {
25484                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25485                        self.advance();
25486                        Some("btrim")
25487                    }
25488                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25489                        self.advance();
25490                        Some("ltrim")
25491                    }
25492                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25493                        self.advance();
25494                        Some("rtrim")
25495                    }
25496                    _ => None,
25497                };
25498                if mode.is_some() || matches!(self.peek(), Token::From) {
25499                    // TRIM([mode] FROM str) — no strip-chars.
25500                    let chars = if matches!(self.peek(), Token::From) {
25501                        None
25502                    } else {
25503                        Some(self.parse_expr(0)?)
25504                    };
25505                    if !matches!(self.peek(), Token::From) {
25506                        return Err(self.err(format!(
25507                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25508                            self.peek()
25509                        )));
25510                    }
25511                    self.advance();
25512                    let target = self.parse_expr(0)?;
25513                    if !matches!(self.peek(), Token::RParen) {
25514                        return Err(
25515                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25516                        );
25517                    }
25518                    self.advance();
25519                    let mut trim_args = alloc::vec![target];
25520                    if let Some(c) = chars {
25521                        trim_args.push(c);
25522                    }
25523                    return Ok(Expr::FunctionCall {
25524                        name: String::from(mode.unwrap_or("btrim")),
25525                        args: trim_args,
25526                    });
25527                }
25528            }
25529            if !matches!(self.peek(), Token::RParen) {
25530                loop {
25531                    // v7.38 (read01, T14) — `argname => value` names this arg.
25532                    // v7.39 (read01 round 77) — `argname := value` is the same
25533                    // thing, and it is the spelling PG's own docs lead with. It
25534                    // was simply never lexed here, so every `f(x := 1)` died in
25535                    // the parser regardless of what `f` was.
25536                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25537                        (
25538                            Token::Ident(n) | Token::QuotedIdent(n),
25539                            Some(Token::FatArrow | Token::ColonEq),
25540                        ) => {
25541                            let name = n.clone();
25542                            self.advance(); // name
25543                            self.advance(); // => / :=
25544                            Some(name)
25545                        }
25546                        _ => None,
25547                    };
25548                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25549                    // array's elements into a variadic call's trailing args
25550                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25551                    // reserved, so it arrives as a bare ident before the arg.
25552                    let is_variadic = this_name.is_none()
25553                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25554                    if is_variadic {
25555                        self.advance();
25556                    }
25557                    let arg = self.parse_expr(0)?;
25558                    args.push(match &this_name {
25559                        // The callee's parameter names decide the slot, and a
25560                        // user function's live in the catalog. Carry the name
25561                        // to eval rather than guessing here.
25562                        Some(n) => Expr::NamedArg {
25563                            name: n.clone(),
25564                            expr: Box::new(arg),
25565                        },
25566                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25567                        None => arg,
25568                    });
25569                    arg_names.push(this_name);
25570                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25571                    // The `::` cast already worked; this lowers the
25572                    // function form onto the same Expr::Cast node.
25573                    if first.eq_ignore_ascii_case("cast")
25574                        && args.len() == 1
25575                        && matches!(self.peek(), Token::As)
25576                    {
25577                        self.advance();
25578                        let target = self.parse_cast_target()?;
25579                        if !matches!(self.peek(), Token::RParen) {
25580                            return Err(self.err(format!(
25581                                "expected ')' to close CAST, got {:?}",
25582                                self.peek()
25583                            )));
25584                        }
25585                        self.advance();
25586                        return Ok(Expr::Cast {
25587                            expr: Box::new(args.pop().expect("one arg")),
25588                            target,
25589                        });
25590                    }
25591                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25592                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25593                    // keywords; SPG's lexer makes them plain idents (so they'd be
25594                    // read as column refs). Lower the keyword to the string form
25595                    // the evaluator already accepts.
25596                    if first.eq_ignore_ascii_case("normalize")
25597                        && args.len() == 1
25598                        && matches!(self.peek(), Token::Comma)
25599                    {
25600                        let form = match self.tokens.get(self.pos + 1) {
25601                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25602                                let up = f.to_ascii_uppercase();
25603                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25604                            }
25605                            _ => None,
25606                        };
25607                        if let Some(up) = form {
25608                            self.advance(); // comma
25609                            self.advance(); // form keyword
25610                            args.push(Expr::Literal(Literal::String(up)));
25611                        }
25612                    }
25613                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25614                    // form. Desugars to the comma-list shape evaluator already
25615                    // handles. Triggered after the first arg when the function
25616                    // name is substring / substr and the next token is FROM
25617                    // (a reserved keyword in PG; SPG also reserves it).
25618                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25619                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25620                    // internal __substring_similar(str, pat, esc) call.
25621                    if (first.eq_ignore_ascii_case("substring")
25622                        || first.eq_ignore_ascii_case("substr"))
25623                        && args.len() == 1
25624                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25625                    {
25626                        self.advance(); // SIMILAR
25627                        let pattern = self.parse_expr(0)?;
25628                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25629                        {
25630                            return Err(self.err(format!(
25631                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25632                                self.peek()
25633                            )));
25634                        }
25635                        self.advance(); // ESCAPE
25636                        let esc = self.parse_expr(0)?;
25637                        if !matches!(self.peek(), Token::RParen) {
25638                            return Err(self.err(format!(
25639                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25640                                self.peek()
25641                            )));
25642                        }
25643                        self.advance();
25644                        args.push(pattern);
25645                        args.push(esc);
25646                        return Ok(Expr::FunctionCall {
25647                            name: "__substring_similar".to_string(),
25648                            args,
25649                        });
25650                    }
25651                    if (first.eq_ignore_ascii_case("substring")
25652                        || first.eq_ignore_ascii_case("substr"))
25653                        && args.len() == 1
25654                        && matches!(self.peek(), Token::From | Token::For)
25655                    {
25656                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25657                        // `substring(str FOR len)` which PG treats as FROM 1.
25658                        if matches!(self.peek(), Token::From) {
25659                            self.advance();
25660                            let start = self.parse_expr(0)?;
25661                            args.push(start);
25662                        } else {
25663                            args.push(Expr::Literal(Literal::Integer(1)));
25664                        }
25665                        if matches!(self.peek(), Token::For) {
25666                            self.advance();
25667                            let length = self.parse_expr(0)?;
25668                            args.push(length);
25669                        }
25670                        if !matches!(self.peek(), Token::RParen) {
25671                            return Err(self.err(format!(
25672                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25673                                self.peek()
25674                            )));
25675                        }
25676                        self.advance();
25677                        return Ok(Expr::FunctionCall {
25678                            name: first.to_ascii_lowercase(),
25679                            args,
25680                        });
25681                    }
25682                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25683                    // syntactic form. Desugars to the `overlay(str,
25684                    // repl, n[, len])` comma-list shape the evaluator
25685                    // already implements. `PLACING` is not a reserved
25686                    // token in SPG, so it arrives as a bare Ident.
25687                    if first.eq_ignore_ascii_case("overlay")
25688                        && args.len() == 1
25689                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25690                    {
25691                        self.advance(); // consume PLACING
25692                        args.push(self.parse_expr(0)?); // replacement
25693                        if !matches!(self.peek(), Token::From) {
25694                            return Err(self.err(format!(
25695                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25696                                self.peek()
25697                            )));
25698                        }
25699                        self.advance();
25700                        args.push(self.parse_expr(0)?); // start position
25701                        if matches!(self.peek(), Token::For) {
25702                            self.advance();
25703                            args.push(self.parse_expr(0)?); // length
25704                        }
25705                        if !matches!(self.peek(), Token::RParen) {
25706                            return Err(self.err(format!(
25707                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25708                                self.peek()
25709                            )));
25710                        }
25711                        self.advance();
25712                        return Ok(Expr::FunctionCall {
25713                            name: String::from("overlay"),
25714                            args,
25715                        });
25716                    }
25717                    // `TRIM(chars FROM str)` — the keyword-less
25718                    // spelling lands here after the chars parse
25719                    // (the keyword forms return earlier).
25720                    if first.eq_ignore_ascii_case("trim")
25721                        && args.len() == 1
25722                        && matches!(self.peek(), Token::From)
25723                    {
25724                        self.advance();
25725                        let target = self.parse_expr(0)?;
25726                        if !matches!(self.peek(), Token::RParen) {
25727                            return Err(self.err(format!(
25728                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25729                                self.peek()
25730                            )));
25731                        }
25732                        self.advance();
25733                        let chars = args.pop().expect("one arg");
25734                        return Ok(Expr::FunctionCall {
25735                            name: String::from("btrim"),
25736                            args: alloc::vec![target, chars],
25737                        });
25738                    }
25739                    // v7.24 (round-16 A) — aggregate-internal
25740                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25741                    // LAST)`. Keys close the argument list.
25742                    if matches!(self.peek(), Token::Order) {
25743                        self.advance();
25744                        if !self.peek_is_by() {
25745                            return Err(self.err(format!(
25746                                "expected BY after ORDER in aggregate args, got {:?}",
25747                                self.peek()
25748                            )));
25749                        }
25750                        self.advance();
25751                        loop {
25752                            // v7.39 (round 691) — save/restore, the discipline this parser
25753                            // already uses around `pending_sample_preds`, so a subquery inside
25754                            // a key neither inherits nor leaks the channel.
25755                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25756                            let saved_coll = self.order_key_collation.take();
25757                            let parsed = self.parse_expr(0);
25758                            self.in_order_by_key = saved_flag;
25759                            let collation =
25760                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25761                            let expr = parsed?;
25762                            let desc = if matches!(self.peek(), Token::Desc) {
25763                                self.advance();
25764                                true
25765                            } else if matches!(self.peek(), Token::Asc) {
25766                                self.advance();
25767                                false
25768                            } else {
25769                                false
25770                            };
25771                            let nulls_first = self.parse_optional_nulls_placement()?;
25772                            agg_order_by.push(OrderBy {
25773                                expr,
25774                                desc,
25775                                nulls_first,
25776                                collation,
25777                            });
25778                            if matches!(self.peek(), Token::Comma) {
25779                                self.advance();
25780                            } else {
25781                                break;
25782                            }
25783                        }
25784                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25785                        // follow the ORDER BY inside GROUP_CONCAT.
25786                        if self.consume_group_concat_separator(&mut args)? {
25787                            saw_separator = true;
25788                        }
25789                        if !matches!(self.peek(), Token::RParen) {
25790                            return Err(self.err(format!(
25791                                "expected ')' after aggregate ORDER BY, got {:?}",
25792                                self.peek()
25793                            )));
25794                        }
25795                        break;
25796                    }
25797                    // v7.39 (round 354, M12) — …or directly after the
25798                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25799                    // own spelling of what PG passes as string_agg's second
25800                    // argument; it was a parse error, so every MySQL query
25801                    // that names its own separator failed outright.
25802                    if self.consume_group_concat_separator(&mut args)? {
25803                        saw_separator = true;
25804                        break;
25805                    }
25806                    match self.peek() {
25807                        Token::Comma => {
25808                            self.advance();
25809                        }
25810                        Token::RParen => break,
25811                        other => {
25812                            return Err(self.err(format!(
25813                                "expected ',' or ')' in function args, got {other:?}"
25814                            )));
25815                        }
25816                    }
25817                }
25818            }
25819            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25820            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25821            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25822            // meaning a separator — that is what the explicit SEPARATOR
25823            // tail is for. Fold them into one `concat(...)` so the
25824            // aggregate keeps its single value argument.
25825            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25826                let values = args.len() - usize::from(saw_separator);
25827                if values > 1 {
25828                    let sep_arg = if saw_separator { args.pop() } else { None };
25829                    let folded = Expr::FunctionCall {
25830                        name: "concat".to_string(),
25831                        args: core::mem::take(&mut args),
25832                    };
25833                    args.push(folded);
25834                    if let Some(sep) = sep_arg {
25835                        args.push(sep);
25836                    }
25837                }
25838            }
25839            self.advance(); // consume ')'
25840            // v7.39 (read01 round 77) — named arguments are NOT reordered here
25841            // any more. The parser has no catalog, so it could only ever resolve
25842            // the handful of `make_*` builtins whose parameter names were baked
25843            // into a table right here — every user function got
25844            // "does not support named arguments", though the catalog has been
25845            // storing its parameter names all along. Reordering happens in eval,
25846            // in one place, for builtins and user functions alike.
25847            // v7.32 (round-29) — ordered-set aggregate tail
25848            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25849            // (percentile_cont / percentile_disc / mode). The sort spec
25850            // lands in the same `order_by` slot a decorated aggregate
25851            // uses; the executor dispatches on the function name. WITHIN
25852            // GROUP and an intra-argument ORDER BY are mutually
25853            // exclusive (PG rejects both).
25854            let within_group_order = self.parse_within_group_clause()?;
25855            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25856                return Err(self.err(
25857                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25858                        .into(),
25859                ));
25860            }
25861            let within_group_seen = !within_group_order.is_empty();
25862            let agg_order_by = if within_group_order.is_empty() {
25863                agg_order_by
25864            } else {
25865                within_group_order
25866            };
25867            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25868            let filter = self.parse_filter_clause()?;
25869            // v4.12: window-function tail — `name(args) OVER (...)`.
25870            // Promotes the just-parsed FunctionCall into a
25871            // WindowFunction node carrying partition + order.
25872            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25873            // / `RESPECT NULLS OVER (...)` between the closing paren
25874            // and `OVER`.
25875            let null_treatment = self.parse_null_treatment_modifier();
25876            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25877                && s.eq_ignore_ascii_case("over")
25878            {
25879                self.advance();
25880                // v7.39 (round 230) — PG implements neither modifier for a
25881                // windowed call and says so (0A000). Both used to be parsed
25882                // and then silently dropped here, so `count(DISTINCT v)
25883                // OVER (…)` quietly answered the non-distinct count.
25884                if agg_distinct {
25885                    return Err(
25886                        self.err("DISTINCT is not implemented for window functions".to_string())
25887                    );
25888                }
25889                if !agg_order_by.is_empty() {
25890                    // PG separates the two shapes that land here: a
25891                    // WITHIN GROUP call is an ordered-set aggregate and gets
25892                    // its own message naming the aggregate; a plain
25893                    // `agg(x ORDER BY y)` gets the generic one.
25894                    let msg = if within_group_seen {
25895                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25896                    } else {
25897                        "aggregate ORDER BY is not implemented for window functions".to_string()
25898                    };
25899                    return Err(self.err(msg));
25900                }
25901                let (partition_by, order_by, frame) = self.parse_over_clause()?;
25902                return Ok(Expr::WindowFunction {
25903                    name: first,
25904                    args,
25905                    partition_by,
25906                    order_by,
25907                    frame,
25908                    null_treatment,
25909                    filter,
25910                });
25911            }
25912            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25913                return Ok(Expr::AggregateOrdered {
25914                    call: Box::new(Expr::FunctionCall { name: first, args }),
25915                    order_by: agg_order_by,
25916                    distinct: agg_distinct,
25917                    filter,
25918                });
25919            }
25920            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25921            // over TIMESTAMPTZ and has no timestamp overload, so a
25922            // timestamp argument is coerced on the way in and the answer
25923            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25924            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25925            // zone`. SPG answered `timestamp without time zone`, dropping
25926            // the offset from every rendering.
25927            //
25928            // Writing the coercion PG performs makes the existing
25929            // argument-driven typing (the one `date_trunc` uses) reach the
25930            // right answer, rather than teaching the type layer a second
25931            // rule. MySQL's DATE_ADD is a different function that returns
25932            // DATE or DATETIME, so this is PG-dialect only.
25933            //
25934            // Out-of-line because this sits on the RECURSIVE descent
25935            // frame: an inline block with locals here costs every nesting
25936            // level, and the suite's deep-nesting sentinel overflowed the
25937            // 512 KiB parser stack the moment one was added (round 430's
25938            // lesson, in the same shape).
25939            if !self.mysql_dialect {
25940                lift_date_add_arg_to_timestamptz(&first, &mut args);
25941            }
25942            return Ok(Expr::FunctionCall { name: first, args });
25943        }
25944        // v7.9.20 — SQL-standard parenless keyword expressions
25945        // (PG treats these as functions called without parens).
25946        // Resolve to a synthetic FunctionCall so the engine's
25947        // eval path reuses the existing function-call routing.
25948        // mailrs G3.
25949        let lc = first.to_ascii_lowercase();
25950        if matches!(
25951            lc.as_str(),
25952            "current_date"
25953                | "current_time"
25954                | "current_timestamp"
25955                | "localtimestamp"
25956                | "localtime"
25957                // v7.37.17 (17.6 siblings) — session-identity SQL-
25958                // standard parenless keywords. current_user /
25959                // session_user / user were already caught by the
25960                // pgwire canned-response shortcut but bare-select
25961                // in the embedded engine went through Expr::Column
25962                // and errored. Adding them here so the parser
25963                // resolves to a synthetic FunctionCall that reuses
25964                // the existing eval/functions.rs dispatch.
25965                | "current_user"
25966                | "session_user"
25967                | "current_role"
25968                | "current_catalog"
25969                | "current_schema"
25970                | "current_database"
25971                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25972                | "system_user"
25973        ) {
25974            return Ok(Expr::FunctionCall {
25975                name: lc,
25976                args: Vec::new(),
25977            });
25978        }
25979        Ok(Expr::Column(ColumnName {
25980            qualifier: None,
25981            name: first,
25982        }))
25983    }
25984}
25985
25986/// v7.39 (round 522) — write the coercion PG's `date_add` /
25987/// `date_subtract` signature performs.
25988///
25989/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25990/// timestamp argument is cast on the way in and the answer is
25991/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25992/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25993/// `timestamp without time zone`, dropping the offset from every
25994/// rendering of the result.
25995///
25996/// Writing the cast the signature implies lets the existing
25997/// argument-driven typing (the one `date_trunc` uses) reach the right
25998/// answer instead of teaching the type layer a second rule. MySQL's
25999/// DATE_ADD is a different function returning DATE or DATETIME, so the
26000/// caller applies this in PG dialect only.
26001///
26002/// A free function, and not a block at the call site, because the caller
26003/// is on the recursive-descent frame chain.
26004#[inline(never)]
26005fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26006    if args.len() != 2
26007        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26008    {
26009        return;
26010    }
26011    let base = args.remove(0);
26012    args.insert(
26013        0,
26014        Expr::Cast {
26015            expr: Box::new(base),
26016            target: CastTarget::Timestamptz,
26017        },
26018    );
26019}
26020
26021/// v6.8.2 — walk an expression tree and return the first column
26022/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26023/// to derive `CreateIndexStatement.column` from an expression
26024/// key (so downstream planner code resolving a primary column
26025/// position keeps working with expression indexes). Returns
26026/// `None` when the expression has no column ref at all — caller
26027/// surfaces that as a parse error.
26028fn extract_first_column(expr: &Expr) -> Option<String> {
26029    match expr {
26030        Expr::Column(cn) => Some(cn.name.clone()),
26031        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26032        Expr::Binary { lhs, rhs, .. } => {
26033            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26034        }
26035        Expr::Unary { expr: e, .. } => extract_first_column(e),
26036        // v7.39 (read01 round 93) — a cast wraps its operand: a common
26037        // expression-index key is `lower(col::text)`, where the column
26038        // sits under the `::text` cast inside the function arg. Without
26039        // descending here the key was rejected as "references no column".
26040        Expr::Cast { expr: e, .. } => extract_first_column(e),
26041        _ => None,
26042    }
26043}
26044
26045fn maybe_not(expr: Expr, negated: bool) -> Expr {
26046    if negated {
26047        Expr::Unary {
26048            op: UnOp::Not,
26049            expr: Box::new(expr),
26050        }
26051    } else {
26052        expr
26053    }
26054}
26055
26056/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26057/// things in the two dialects, and SPG read all three PG's way:
26058///
26059/// | token | PG (and SPG) | MySQL, measured |
26060/// |---|---|---|
26061/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26062/// | `&&` | inet / array overlap | **AND** |
26063/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26064///
26065/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26066/// answer with no error, which is why they are routed here rather than
26067/// left to the shared table.
26068impl Parser {
26069    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26070        if self.mysql_dialect {
26071            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26072            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26073            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26074            if let Token::Ident(w) = tok
26075                && w.eq_ignore_ascii_case("div")
26076            {
26077                return Some((BinOp::IntDiv, 8));
26078            }
26079            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26080            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26081            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26082            // there sits in operand position, not infix).
26083            if let Token::Ident(w) = tok
26084                && w.eq_ignore_ascii_case("mod")
26085            {
26086                return Some((BinOp::Mod, 8));
26087            }
26088            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26089            // plain ident to the lexer. Its precedence sits between OR (1)
26090            // and AND (3) — hence rung 2, the slot freed by moving AND up.
26091            if let Token::Ident(w) = tok
26092                && w.eq_ignore_ascii_case("xor")
26093            {
26094                return Some((BinOp::LogicalXor, 2));
26095            }
26096            match tok {
26097                Token::Concat => return Some((BinOp::Or, 1)),
26098                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26099                Token::InetOverlap => return Some((BinOp::And, 3)),
26100                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26101                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26102                _ => {}
26103            }
26104        }
26105        binop_from(tok)
26106    }
26107}
26108
26109// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26110// (which sits strictly between OR and AND), every level from AND upward was
26111// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26112// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26113// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26114// the *relative* order of every PG operator is unchanged by the shift.
26115fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26116    let pair = match tok {
26117        Token::Or => (BinOp::Or, 1),
26118        Token::And => (BinOp::And, 3),
26119        Token::Eq => (BinOp::Eq, 5),
26120        Token::NotEq => (BinOp::NotEq, 5),
26121        Token::Lt => (BinOp::Lt, 5),
26122        Token::LtEq => (BinOp::LtEq, 5),
26123        Token::Gt => (BinOp::Gt, 5),
26124        Token::GtEq => (BinOp::GtEq, 5),
26125        // pgvector distance ops all sit on the same rung — tighter than
26126        // comparisons (5) so `col <-> v < threshold` parses correctly.
26127        Token::L2Distance => (BinOp::L2Distance, 6),
26128        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26129        // comparison rung.
26130        Token::GeomParallel => (BinOp::GeomParallel, 5),
26131        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26132        // comparison rung.
26133        Token::OverLeft => (BinOp::OverLeft, 5),
26134        Token::OverRight => (BinOp::OverRight, 5),
26135        Token::GeomPerp => (BinOp::GeomPerp, 5),
26136        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26137        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26138        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26139        Token::InnerProduct => (BinOp::InnerProduct, 6),
26140        Token::CosineDistance => (BinOp::CosineDistance, 6),
26141        Token::Plus => (BinOp::Add, 7),
26142        Token::Minus => (BinOp::Sub, 7),
26143        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26144        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26145        // pgvector distances above) BETWEEN additive (7) and the
26146        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26147        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26148        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26149        // ("matches PG conceptually" — the round-753 audit measured it
26150        // false; the old rung errored on `'a' || 1 + 1` with
26151        // `text + integer`). Same-level chains left-fold, as PG does.
26152        Token::Concat => (BinOp::Concat, 6),
26153        Token::Pipe => (BinOp::BitOr, 6),
26154        Token::Amp => (BinOp::BitAnd, 6),
26155        Token::Star => (BinOp::Mul, 8),
26156        Token::Slash => (BinOp::Div, 8),
26157        Token::Percent => (BinOp::Mod, 8),
26158        // v4.14: JSON path ops bind tighter than comparisons (5)
26159        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26160        // Same rung as the multiplicative ops.
26161        Token::JsonGet => (BinOp::JsonGet, 8),
26162        Token::JsonGetText => (BinOp::JsonGetText, 8),
26163        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26164        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26165        Token::JsonContains => (BinOp::JsonContains, 8),
26166        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26167        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26168        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26169        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26170        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26171        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26172        // v7.12.2 — `@@` binds at the comparison rung (looser than
26173        // arithmetic, tighter than AND / OR). PG places `@@` at
26174        // the same precedence as `=` / `<`, so we follow.
26175        Token::TsMatch => (BinOp::TsMatch, 5),
26176        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26177        // PG places these at the comparison rung (same level as `=`),
26178        // so we follow.
26179        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26180        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26181        Token::InetContains => (BinOp::InetContains, 5),
26182        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26183        Token::InetOverlap => (BinOp::InetOverlap, 5),
26184        // v7.39 (round 508) — the geometric and pattern-order predicates
26185        // ride the comparison rung, as every other predicate does.
26186        Token::Intersects => (BinOp::Intersects, 5),
26187        Token::IsBelow => (BinOp::IsBelow, 5),
26188        Token::IsAbove => (BinOp::IsAbove, 5),
26189        Token::PatternLt => (BinOp::PatternLt, 5),
26190        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26191        Token::PatternGt => (BinOp::PatternGt, 5),
26192        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26193        // `@@@` is the old spelling of `@@` and means exactly it.
26194        Token::TsMatchOld => (BinOp::TsMatch, 5),
26195        _ => return None,
26196    };
26197    Some(pair)
26198}
26199
26200#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26201// `as f32` here is intentional: vector elements widen / narrow into f32 on
26202// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26203// past ~15 decimal digits — both are acceptable for a fixed-precision
26204// pgvector column.
26205/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26206/// implicit table alias and break trailing clauses. WITH lands
26207/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26208/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26209/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26210/// / VALUES / FOR / LATERAL — all of which would otherwise be
26211/// silently swallowed by `parse_optional_alias`.
26212fn is_alias_stopword(s: &str) -> bool {
26213    matches!(
26214        s.to_ascii_lowercase().as_str(),
26215        "with"
26216            | "on"
26217            | "where"
26218            | "having"
26219            | "group"
26220            | "order"
26221            | "limit"
26222            | "offset"
26223            | "union"
26224            | "except"
26225            | "intersect"
26226            | "returning"
26227            | "set"
26228            | "values"
26229            | "for"
26230            | "window"
26231            | "tablesample"
26232            | "lateral"
26233            | "left"
26234            | "right"
26235            | "inner"
26236            | "outer"
26237            | "full"
26238            | "cross"
26239            | "join"
26240            | "natural"
26241            | "using"
26242            | "fetch"
26243    )
26244}
26245
26246fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26247    match e {
26248        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26249        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26250        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26251        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26252        // so scale the divisor by hand instead of `f32::powi`.)
26253        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26254            let mut div = 1.0f32;
26255            for _ in 0..*scale {
26256                div *= 10.0;
26257            }
26258            Some(*unscaled as f32 / div)
26259        }
26260        Expr::Unary {
26261            op: UnOp::Neg,
26262            expr,
26263        } => extract_numeric_literal(expr).map(|x| -x),
26264        _ => None,
26265    }
26266}
26267
26268/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26269/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26270/// negative. Returns `None` if any pair fails to parse or no pair is found.
26271///
26272/// Recognised units (case-insensitive, optional trailing `s`):
26273/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26274/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26275/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26276/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26277/// (PG-canonical: DST and month-boundary semantics depend on this).
26278/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26279/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26280/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26281#[allow(clippy::cast_possible_truncation)]
26282fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26283    let mut months: i64 = 0;
26284    let mut days: i64 = 0;
26285    let mut micros: i64 = 0;
26286    let mut in_time = false;
26287    let mut num = alloc::string::String::new();
26288    for ch in rest.chars() {
26289        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26290            num.push(ch);
26291            continue;
26292        }
26293        if ch == 'T' || ch == 't' {
26294            if !num.is_empty() {
26295                return None;
26296            }
26297            in_time = true;
26298            continue;
26299        }
26300        let n: f64 = num.parse().ok()?;
26301        num.clear();
26302        match (ch, in_time) {
26303            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26304            ('M', false) => months += n as i64,
26305            ('W' | 'w', false) => days += (n * 7.0) as i64,
26306            ('D' | 'd', false) => days += n as i64,
26307            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26308            ('M', true) => micros += (n * 60_000_000.0) as i64,
26309            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26310            _ => return None,
26311        }
26312    }
26313    if !num.is_empty() {
26314        return None;
26315    }
26316    Some((
26317        i32::try_from(months).ok()?,
26318        i32::try_from(days).ok()?,
26319        micros,
26320    ))
26321}
26322
26323/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26324/// leading `-` negates the whole value). Rejects date-like strings.
26325fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26326    let (neg, body) = match s.strip_prefix('-') {
26327        Some(b) => (true, b),
26328        None => (false, s),
26329    };
26330    let (y, m) = body.split_once('-')?;
26331    let years: i32 = y.parse().ok()?;
26332    let mons: i32 = m.parse().ok()?;
26333    if years < 0 || mons < 0 {
26334        return None;
26335    }
26336    let total = years.checked_mul(12)?.checked_add(mons)?;
26337    Some((if neg { -total } else { total }, 0, 0))
26338}
26339
26340/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26341/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26342fn parse_interval_clock(tok: &str) -> Option<i64> {
26343    let (neg, body) = match tok.strip_prefix('-') {
26344        Some(r) => (true, r),
26345        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26346    };
26347    let mut it = body.split(':');
26348    let h: i64 = it.next()?.parse().ok()?;
26349    let m: i64 = it.next()?.parse().ok()?;
26350    let s_tok = it.next().unwrap_or("0");
26351    if it.next().is_some() {
26352        return None;
26353    }
26354    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26355        let sec: i64 = sec.parse().ok()?;
26356        let mut f = alloc::string::String::from(frac);
26357        while f.len() < 6 {
26358            f.push('0');
26359        }
26360        f.truncate(6);
26361        let fus: i64 = f.parse().ok()?;
26362        sec.checked_mul(1_000_000)?.checked_add(fus)?
26363    } else {
26364        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26365    };
26366    let total = h
26367        .checked_mul(3_600_000_000)?
26368        .checked_add(m.checked_mul(60_000_000)?)?
26369        .checked_add(sec_us)?;
26370    Some(if neg { -total } else { total })
26371}
26372
26373/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26374/// every spelling PG accepts (measured against live PG18.4, not guessed):
26375/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26376/// Before this, the unit table matched long names only, with an ad-hoc
26377/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26378/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26379/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26380/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26381/// fractional) both read from this one table now.
26382fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26383    let u = raw.to_ascii_lowercase();
26384    Some(match u.as_str() {
26385        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26386            "microsecond"
26387        }
26388        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26389            "millisecond"
26390        }
26391        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26392        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26393        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26394        "day" | "days" | "d" => "day",
26395        "week" | "weeks" | "w" => "week",
26396        "month" | "months" | "mon" | "mons" => "month",
26397        "year" | "years" | "yr" | "yrs" | "y" => "year",
26398        "decade" | "decades" | "dec" | "decs" => "decade",
26399        "century" | "centuries" | "cent" | "c" => "century",
26400        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26401        _ => return None,
26402    })
26403}
26404
26405/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26406/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26408pub(crate) enum IntervalField {
26409    Year,
26410    Month,
26411    Day,
26412    Hour,
26413    Minute,
26414    Second,
26415}
26416
26417/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26418/// spellings aren't standard for the qualifier position, so only the singular
26419/// forms are accepted.
26420/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26421/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26422/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26423/// take a `'1 2'` style literal — are not read here; they stay a parse
26424/// error rather than being silently misread.)
26425/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26426///
26427/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26428/// to do with a `@@` engine setting, and an unset one reads NULL rather
26429/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26430/// were the same node and `SELECT @x` answered "Unknown system variable".)
26431/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26432/// not see a session override — measured, after `SET autocommit=0`,
26433/// `@@global.autocommit` is still 1.
26434///
26435/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26436/// the parser's nesting budget is tuned against, and building these
26437/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26438/// wall `parse_left_right_atom` and friends were factored out for).
26439#[inline(never)]
26440fn variable_ref_atom(raw: &str) -> Expr {
26441    let user_var = !raw.starts_with("@@");
26442    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26443    Expr::FunctionCall {
26444        name: String::from(if user_var {
26445            "__spg_user_var"
26446        } else {
26447            "__spg_session_var"
26448        }),
26449        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26450    }
26451}
26452
26453fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26454    let Token::Ident(s) = tok else { return None };
26455    Some(match () {
26456        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26457        () if s.eq_ignore_ascii_case("second") => "second",
26458        () if s.eq_ignore_ascii_case("minute") => "minute",
26459        () if s.eq_ignore_ascii_case("hour") => "hour",
26460        () if s.eq_ignore_ascii_case("day") => "day",
26461        () if s.eq_ignore_ascii_case("week") => "week",
26462        () if s.eq_ignore_ascii_case("month") => "month",
26463        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26464        () if s.eq_ignore_ascii_case("year") => "year",
26465        () => return None,
26466    })
26467}
26468
26469/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26470/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26471/// which constructs the value at run time. Only the slot the unit names
26472/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26473/// slot the builtin has (months and fractional seconds respectively).
26474fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26475    let zero = || Expr::Literal(Literal::Integer(0));
26476    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26477        lhs: alloc::boxed::Box::new(qty.clone()),
26478        op,
26479        rhs: alloc::boxed::Box::new(by),
26480    };
26481    // (years, months, weeks, days, hours, mins, secs)
26482    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26483    match unit {
26484        "year" => args[0] = qty,
26485        "quarter" => {
26486            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26487        }
26488        "month" => args[1] = qty,
26489        "week" => args[2] = qty,
26490        "day" => args[3] = qty,
26491        "hour" => args[4] = qty,
26492        "minute" => args[5] = qty,
26493        "second" => args[6] = qty,
26494        // The builtin's seconds slot takes a fraction, so microseconds ride
26495        // it scaled down; the divisor is a NUMERIC literal so the division
26496        // stays exact rather than going through a float.
26497        "microsecond" => {
26498            args[6] = scaled(
26499                crate::ast::BinOp::Div,
26500                Expr::Literal(Literal::Numeric {
26501                    unscaled: 1_000_000,
26502                    scale: 0,
26503                }),
26504            );
26505        }
26506        _ => args[3] = qty,
26507    }
26508    Expr::FunctionCall {
26509        name: alloc::string::String::from("make_interval"),
26510        args,
26511    }
26512}
26513
26514/// `(count, unit)` → `(months, days, micros)`.
26515fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26516    let n: i64 = count.trim().parse().ok()?;
26517    Some(match unit {
26518        "microsecond" => (0, 0, n),
26519        "second" => (0, 0, n.checked_mul(1_000_000)?),
26520        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26521        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26522        "day" => (0, i32::try_from(n).ok()?, 0),
26523        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26524        "month" => (i32::try_from(n).ok()?, 0, 0),
26525        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26526        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26527        _ => return None,
26528    })
26529}
26530
26531fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26532    let Token::Ident(s) = tok else { return None };
26533    Some(match () {
26534        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26535        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26536        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26537        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26538        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26539        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26540        () => return None,
26541    })
26542}
26543
26544/// v7.39 (read01 round 102) — interpret an interval literal under a field
26545/// qualifier. Returns `(months, days, micros)`.
26546///
26547/// * A single field applied to a bare number sets which unit the number means,
26548///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26549///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26550/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26551/// * Every other range, and any literal a single field can't read as a plain
26552///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26553///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26554///   like PG, and the qualifier there only bounds precision.
26555fn interpret_qualified_interval(
26556    text: &str,
26557    (f1, f2): (IntervalField, Option<IntervalField>),
26558) -> Option<(i32, i32, i64)> {
26559    if let Some(f2) = f2 {
26560        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26561            if let Some(m) = parse_year_month_literal(text) {
26562                return Some((m, 0, 0));
26563            }
26564        }
26565        return parse_interval_text(text);
26566    }
26567    // Single field: reinterpret a bare number; otherwise the default parse.
26568    let trimmed = text.trim();
26569    if let Ok(val) = trimmed.parse::<f64>() {
26570        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26571        #[allow(clippy::cast_possible_truncation)]
26572        let whole = val as i64;
26573        #[allow(clippy::cast_possible_truncation)]
26574        let secs_micros = {
26575            let m = val * 1_000_000.0;
26576            if m >= 0.0 {
26577                (m + 0.5) as i64
26578            } else {
26579                (m - 0.5) as i64
26580            }
26581        };
26582        return Some(match f1 {
26583            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26584            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26585            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26586            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26587            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26588            IntervalField::Second => (0, 0, secs_micros),
26589        });
26590    }
26591    parse_interval_text(text)
26592}
26593
26594/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26595fn parse_year_month_literal(text: &str) -> Option<i32> {
26596    let t = text.trim();
26597    let (neg, body) = match t.strip_prefix('-') {
26598        Some(r) => (true, r),
26599        None => (false, t.strip_prefix('+').unwrap_or(t)),
26600    };
26601    let mut it = body.split('-');
26602    let years: i32 = it.next()?.trim().parse().ok()?;
26603    let months: i32 = match it.next() {
26604        Some(m) => m.trim().parse().ok()?,
26605        None => 0,
26606    };
26607    if it.next().is_some() {
26608        return None;
26609    }
26610    let total = years.checked_mul(12)?.checked_add(months)?;
26611    Some(if neg { -total } else { total })
26612}
26613
26614pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26615    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26616    // `@` is decorative; a trailing `ago` negates the whole interval.
26617    let mut trimmed = s.trim();
26618    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26619    let mut negate = false;
26620    if let Some(rest) = trimmed
26621        .strip_suffix("ago")
26622        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26623    {
26624        negate = true;
26625        trimmed = rest.trim();
26626    }
26627    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26628        let (mo, d, us) = v?;
26629        if negate {
26630            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26631        } else {
26632            Some((mo, d, us))
26633        }
26634    };
26635    let s = trimmed;
26636    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26637    // are single tokens, not the `<n> <unit>` pair form handled below.
26638    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26639        return finish(parse_iso8601_interval(rest));
26640    }
26641    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26642        if let Some(iv) = parse_year_month_interval(trimmed) {
26643            return finish(Some(iv));
26644        }
26645    }
26646    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26647    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26648    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26649    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26650        if let Ok(n) = trimmed.parse::<i64>() {
26651            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26652        }
26653        if let Ok(f) = trimmed.parse::<f64>() {
26654            if f.is_finite() {
26655                #[allow(clippy::cast_possible_truncation)]
26656                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26657            }
26658        }
26659    }
26660    // v7.39 (round 243) — PG accepts the number and unit run together
26661    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26662    // the `<n> <unit>` pair loop below sees them as two.
26663    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26664    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26665    for p in raw_parts {
26666        let boundary = p
26667            .char_indices()
26668            .find(|(i, c)| {
26669                *i > 0
26670                    && c.is_ascii_alphabetic()
26671                    && p[..*i]
26672                        .chars()
26673                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26674                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26675            })
26676            .map(|(i, _)| i);
26677        match boundary {
26678            Some(i) => {
26679                parts.push(&p[..i]);
26680                parts.push(&p[i..]);
26681            }
26682            None => parts.push(p),
26683        }
26684    }
26685    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26686    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26687    // remains is the `<n> <unit>` pair form handled below.
26688    let mut clock_us: i64 = 0;
26689    let mut had_clock = false;
26690    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26691        clock_us = parse_interval_clock(parts[pos])?;
26692        parts.remove(pos);
26693        had_clock = true;
26694    }
26695    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26696    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26697    let mut lone_days: i32 = 0;
26698    if had_clock && parts.len() == 1 {
26699        if let Ok(n) = parts[0].parse::<i64>() {
26700            lone_days = i32::try_from(n).ok()?;
26701            parts.clear();
26702        }
26703    }
26704    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26705        return None;
26706    }
26707    let mut months: i32 = 0;
26708    let mut days: i32 = lone_days;
26709    let mut micros: i64 = clock_us;
26710    let mut i = 0;
26711    while i < parts.len() {
26712        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26713        if let Ok(n) = parts[i].parse::<i64>() {
26714            match unit_stripped {
26715                "microsecond" => micros = micros.checked_add(n)?,
26716                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26717                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26718                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26719                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26720                "day" => {
26721                    let n32 = i32::try_from(n).ok()?;
26722                    days = days.checked_add(n32)?;
26723                }
26724                "week" => {
26725                    let n32 = i32::try_from(n).ok()?;
26726                    days = days.checked_add(n32.checked_mul(7)?)?;
26727                }
26728                "month" => {
26729                    let n32 = i32::try_from(n).ok()?;
26730                    months = months.checked_add(n32)?;
26731                }
26732                "year" => {
26733                    let n32 = i32::try_from(n).ok()?;
26734                    months = months.checked_add(n32.checked_mul(12)?)?;
26735                }
26736                // v7.39 (read01 timestamp.c) — the larger calendar units.
26737                "decade" => {
26738                    let n32 = i32::try_from(n).ok()?;
26739                    months = months.checked_add(n32.checked_mul(120)?)?;
26740                }
26741                "century" => {
26742                    let n32 = i32::try_from(n).ok()?;
26743                    months = months.checked_add(n32.checked_mul(1200)?)?;
26744                }
26745                "millennium" => {
26746                    let n32 = i32::try_from(n).ok()?;
26747                    months = months.checked_add(n32.checked_mul(12000)?)?;
26748                }
26749                _ => return None,
26750            }
26751        } else if let Ok(f) = parts[i].parse::<f64>() {
26752            // Fractional units cascade down to the next-finer field the way
26753            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26754            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26755            // no_std: f64 has no trunc/fract/round methods, so do them with
26756            // casts (toward-zero) + explicit round-half-away-from-zero.
26757            #[allow(clippy::cast_possible_truncation)]
26758            fn round_i64(x: f64) -> i64 {
26759                if x >= 0.0 {
26760                    (x + 0.5) as i64
26761                } else {
26762                    (x - 0.5) as i64
26763                }
26764            }
26765            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26766            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26767                const DAY_US: f64 = 86_400_000_000.0;
26768                let whole = d as i64; // truncates toward zero
26769                let frac = d - whole as f64;
26770                *days = days.checked_add(i32::try_from(whole).ok()?)?;
26771                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26772                Some(())
26773            }
26774            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26775            match unit_stripped {
26776                "microsecond" => micros = micros.checked_add(round_i64(f))?,
26777                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26778                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26779                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26780                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26781                "day" => add_days_frac(&mut days, &mut micros, f)?,
26782                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26783                "month" => {
26784                    let whole = f as i64;
26785                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26786                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26787                }
26788                "year" => {
26789                    let m = f * 12.0;
26790                    let whole = m as i64;
26791                    months = months.checked_add(i32::try_from(whole).ok()?)?;
26792                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26793                }
26794                _ => return None,
26795            }
26796        } else {
26797            return None;
26798        }
26799        i += 2;
26800    }
26801    finish(Some((months, days, micros)))
26802}
26803
26804/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26805/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26806/// `interval` is intentionally absent (handled by its own parser arm).
26807/// Returns `None` for names that aren't sensible as a bare typed literal, so
26808/// the caller falls back to treating the ident as a column reference.
26809fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26810    Some(match ident {
26811        "date" => CastTarget::Date,
26812        "timestamp" | "datetime" => CastTarget::Timestamp,
26813        "timestamptz" => CastTarget::Timestamptz,
26814        "bool" | "boolean" => CastTarget::Bool,
26815        "int" | "integer" | "int4" => CastTarget::Int,
26816        "bigint" | "int8" => CastTarget::BigInt,
26817        "float8" | "double precision" => CastTarget::Float,
26818        "uuid" => CastTarget::Uuid,
26819        "bytea" => CastTarget::Bytea,
26820        "json" => CastTarget::Json,
26821        "jsonb" => CastTarget::Jsonb,
26822        // Types without a dedicated CastTarget variant flow through the
26823        // generic Named path (engine resolves via column_type_to_data_type).
26824        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26825        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26826        | "money" | "bit" | "varbit"
26827        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26828        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26829        // Range / multirange types likewise.
26830        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26831        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26832        | "datemultirange" | "tsmultirange" | "tstzmultirange"
26833        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26834        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26835            CastTarget::Named(alloc::string::String::from(ident))
26836        }
26837        _ => return None,
26838    })
26839}
26840
26841/// v7.12.4 — map a bare type-name identifier (the form that
26842/// appears in a function arg list or RETURNS clause) to a
26843/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26844/// types so the caller can preserve them as
26845/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26846///
26847/// Subset of the full column-type grammar — we deliberately
26848/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26849/// here because function-arg types in v7.12.4 are mostly the
26850/// bare form (`text`, `int`, `bytea`, …).
26851/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26852/// than being `name TYPE`?
26853///
26854/// The multi-word spellings SQL allows for a bare argument type, each
26855/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26856///
26857/// NOTE this list also exists in `spg-storage`, which computes the
26858/// signature key from the rendered argument text and has to reach the
26859/// same verdict. The two crates are siblings — neither depends on the
26860/// other — and each already carries its own table of type spellings
26861/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26862/// there), so this follows the structure rather than inventing new
26863/// duplication. Recorded as V49.
26864pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26865    let t = phrase.trim().to_ascii_lowercase();
26866    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26867    matches!(
26868        base,
26869        "double precision"
26870            | "character varying"
26871            | "bit varying"
26872            | "timestamp with time zone"
26873            | "timestamp without time zone"
26874            | "time with time zone"
26875            | "time without time zone"
26876            | "national character"
26877            | "national character varying"
26878    )
26879}
26880
26881fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26882    Some(match ident.to_ascii_lowercase().as_str() {
26883        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26884        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26885        "bigint" => ColumnTypeName::BigInt,
26886        "float" | "double" => ColumnTypeName::Float,
26887        // v7.39 (round 269) — real is 32-bit.
26888        "real" | "float4" => ColumnTypeName::Real,
26889        "text" => ColumnTypeName::Text,
26890        "bool" | "boolean" => ColumnTypeName::Bool,
26891        "date" => ColumnTypeName::Date,
26892        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26893        "timestamptz" => ColumnTypeName::Timestamptz,
26894        "json" => ColumnTypeName::Json,
26895        "jsonb" => ColumnTypeName::Jsonb,
26896        "bytea" | "bytes" => ColumnTypeName::Bytes,
26897        "tsvector" => ColumnTypeName::TsVector,
26898        "tsquery" => ColumnTypeName::TsQuery,
26899        "uuid" => ColumnTypeName::Uuid,
26900        "interval" => ColumnTypeName::Interval,
26901        "time" => ColumnTypeName::Time,
26902        "year" => ColumnTypeName::Year,
26903        "timetz" => ColumnTypeName::TimeTz,
26904        "money" => ColumnTypeName::Money,
26905        _ => return None,
26906    })
26907}
26908
26909/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26910/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26911///
26912/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26913/// / embedded SQL land in v7.12.5+):
26914///
26915/// ```text
26916///   body          := [ws] block [ws]
26917///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26918///   stmt          := assign | return
26919///   assign        := assign_target := expr
26920///   assign_target := ( NEW | OLD ) . ident | ident
26921///   return        := RETURN ( NEW | OLD | NULL | expr )
26922/// ```
26923///
26924/// `expr` is parsed by recursing into the regular `Parser` — so a
26925/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26926/// NEW.subject || ' ' || NEW.sender)` body shape works without
26927/// the body parser knowing what `to_tsvector` is.
26928///
26929/// Errors here cause the caller to fall back to
26930/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26931/// successful, but the executor will refuse to invoke the
26932/// function with an "unparseable body" error.
26933/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26934/// from the crate root as `spg_sql::parse_function_body`.
26935pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26936    parse_plpgsql_body(body)
26937}
26938
26939fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26940    // Use the regular lexer on the body text. The trailing
26941    // `END;` may or may not have a semicolon; the lexer treats
26942    // both forms identically.
26943    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26944        message: alloc::format!("plpgsql body lex error: {e}"),
26945        token_pos: 0,
26946    })?;
26947    let mut parser = Parser::new(tokens);
26948    parser.parse_plpgsql_block()
26949}
26950
26951/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26952fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26953    match v {
26954        crate::ast::SetValue::String(s)
26955        | crate::ast::SetValue::Ident(s)
26956        | crate::ast::SetValue::Number(s) => s.clone(),
26957        crate::ast::SetValue::Default => "DEFAULT".into(),
26958    }
26959}
26960
26961/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26962/// contains an aggregate call at ITS OWN query level (recursion stops at
26963/// sublink boundaries — a sublink's aggregates belong to the sublink).
26964/// Backs the "aggregate functions are not allowed in a recursive query's
26965/// recursive term" well-formedness check.
26966fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26967    const AGG_NAMES: &[&str] = &[
26968        "count",
26969        "sum",
26970        "min",
26971        "max",
26972        "avg",
26973        "string_agg",
26974        "array_agg",
26975        "bool_and",
26976        "bool_or",
26977        "every",
26978        "any_value",
26979        "json_agg",
26980        "jsonb_agg",
26981        "json_object_agg",
26982        "jsonb_object_agg",
26983        "bit_and",
26984        "bit_or",
26985        "bit_xor",
26986        "var_pop",
26987        "var_samp",
26988        "variance",
26989        "stddev",
26990        "stddev_pop",
26991        "stddev_samp",
26992        "range_agg",
26993        "range_intersect_agg",
26994        "percentile_cont",
26995        "percentile_disc",
26996        "mode",
26997        "corr",
26998        "covar_pop",
26999        "covar_samp",
27000    ];
27001    match e {
27002        Expr::AggregateOrdered { .. } => true,
27003        Expr::FunctionCall { name, args } => {
27004            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27005                || args.iter().any(expr_has_toplevel_aggregate)
27006        }
27007        Expr::NamedArg { expr, .. }
27008        | Expr::Variadic(expr)
27009        | Expr::Unary { expr, .. }
27010        | Expr::Cast { expr, .. }
27011        | Expr::IsNull { expr, .. }
27012        | Expr::FieldAccess { base: expr, .. }
27013        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27014        Expr::Binary { lhs, rhs, .. } => {
27015            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27016        }
27017        Expr::Like { expr, pattern, .. } => {
27018            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27019        }
27020        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27021        Expr::InList { expr, list, .. } => {
27022            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27023        }
27024        Expr::ArraySubscript { target, index } => {
27025            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27026        }
27027        Expr::ArraySlice { target, lo, hi } => {
27028            expr_has_toplevel_aggregate(target)
27029                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27030                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27031        }
27032        Expr::AnyAll { expr, array, .. } => {
27033            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27034        }
27035        Expr::Case {
27036            operand,
27037            branches,
27038            else_branch,
27039        } => {
27040            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27041                || branches
27042                    .iter()
27043                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27044                || else_branch
27045                    .as_deref()
27046                    .is_some_and(expr_has_toplevel_aggregate)
27047        }
27048        // The outer-level operands of a sublink can aggregate; the sublink's
27049        // own body cannot leak its aggregates up here.
27050        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27051        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27052            row.iter().any(expr_has_toplevel_aggregate)
27053        }
27054        _ => false,
27055    }
27056}
27057
27058/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27059/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27060/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27061/// sublink and is legal in a recursive term, so it is not walked here.
27062fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27063    let mut exprs: Vec<&Expr> = Vec::new();
27064    for it in &s.items {
27065        if let crate::ast::SelectItem::Expr { expr, .. } = it {
27066            exprs.push(expr);
27067        }
27068    }
27069    if let Some(w) = &s.where_ {
27070        exprs.push(w);
27071    }
27072    if let Some(h) = &s.having {
27073        exprs.push(h);
27074    }
27075    if let Some(g) = &s.group_by {
27076        exprs.extend(g.iter());
27077    }
27078    if let Some(from) = &s.from {
27079        for j in &from.joins {
27080            if let Some(on) = &j.on {
27081                exprs.push(on);
27082            }
27083        }
27084    }
27085    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27086}
27087
27088/// Does this expression contain a sublink whose subquery mentions `name`?
27089fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27090    match e {
27091        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27092        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27093        Expr::InSubquery { expr, subquery, .. } => {
27094            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27095        }
27096        Expr::RowInSubquery { row, subquery, .. } => {
27097            row.iter().any(|x| expr_sublink_mentions(x, name))
27098                || select_mentions_table(subquery, name)
27099        }
27100        Expr::RowCmpSubquery { row, subquery, .. } => {
27101            row.iter().any(|x| expr_sublink_mentions(x, name))
27102                || select_mentions_table(subquery, name)
27103        }
27104        Expr::NamedArg { expr, .. }
27105        | Expr::Variadic(expr)
27106        | Expr::Unary { expr, .. }
27107        | Expr::Cast { expr, .. }
27108        | Expr::IsNull { expr, .. }
27109        | Expr::FieldAccess { base: expr, .. }
27110        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27111        Expr::Binary { lhs, rhs, .. } => {
27112            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27113        }
27114        Expr::Like { expr, pattern, .. } => {
27115            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27116        }
27117        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27118            args.iter().any(|x| expr_sublink_mentions(x, name))
27119        }
27120        Expr::InList { expr, list, .. } => {
27121            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27122        }
27123        Expr::ArraySubscript { target, index } => {
27124            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27125        }
27126        Expr::ArraySlice { target, lo, hi } => {
27127            expr_sublink_mentions(target, name)
27128                || lo
27129                    .as_deref()
27130                    .is_some_and(|x| expr_sublink_mentions(x, name))
27131                || hi
27132                    .as_deref()
27133                    .is_some_and(|x| expr_sublink_mentions(x, name))
27134        }
27135        Expr::AnyAll { expr, array, .. } => {
27136            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27137        }
27138        Expr::Case {
27139            operand,
27140            branches,
27141            else_branch,
27142        } => {
27143            operand
27144                .as_deref()
27145                .is_some_and(|x| expr_sublink_mentions(x, name))
27146                || branches
27147                    .iter()
27148                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27149                || else_branch
27150                    .as_deref()
27151                    .is_some_and(|x| expr_sublink_mentions(x, name))
27152        }
27153        _ => false,
27154    }
27155}
27156
27157/// Does this SELECT (in full — FROM tables, derived tables, its own
27158/// sublinks, and union arms) mention the named table?
27159fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27160    if let Some(from) = &s.from {
27161        if from.primary.name.eq_ignore_ascii_case(name) {
27162            return true;
27163        }
27164        if let Some(sub) = &from.primary.lateral_subquery
27165            && select_mentions_table(sub, name)
27166        {
27167            return true;
27168        }
27169        for j in &from.joins {
27170            if j.table.name.eq_ignore_ascii_case(name) {
27171                return true;
27172            }
27173            if let Some(sub) = &j.table.lateral_subquery
27174                && select_mentions_table(sub, name)
27175            {
27176                return true;
27177            }
27178        }
27179    }
27180    if select_has_self_ref_in_sublink(s, name) {
27181        return true;
27182    }
27183    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27184}
27185
27186/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27187/// row count, the way PG evaluates one before applying it.
27188///
27189/// `None` = not a constant (a column, a subquery, a function call).
27190/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27191/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27192/// All wordings were read off live PG 18.4.
27193fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27194    use crate::ast::{BinOp, Expr, Literal, UnOp};
27195    match e {
27196        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27197        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27198            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27199        }
27200        // PG coerces a string by its CONTENT, and fails on the value.
27201        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27202            |_| {
27203                Err(alloc::format!(
27204                    "invalid input syntax for type bigint: \"{t}\""
27205                ))
27206            },
27207            |n| Ok(i128::from(n)),
27208        )),
27209        Expr::Literal(Literal::Bool(_)) => Some(Err(
27210            "argument of {L} must be type bigint, not type boolean".into(),
27211        )),
27212        Expr::Unary {
27213            op: UnOp::Neg,
27214            expr,
27215        } => match fold_limit_constant(expr)? {
27216            Ok(v) => Some(Ok(-v)),
27217            e @ Err(_) => Some(e),
27218        },
27219        Expr::Binary { lhs, op, rhs } => {
27220            let a = match fold_limit_constant(lhs)? {
27221                Ok(v) => v,
27222                e @ Err(_) => return Some(e),
27223            };
27224            let b = match fold_limit_constant(rhs)? {
27225                Ok(v) => v,
27226                e @ Err(_) => return Some(e),
27227            };
27228            let out = match op {
27229                BinOp::Add => a.checked_add(b),
27230                BinOp::Sub => a.checked_sub(b),
27231                BinOp::Mul => a.checked_mul(b),
27232                BinOp::Div if b != 0 => a.checked_div(b),
27233                BinOp::Div => return Some(Err("division by zero".into())),
27234                BinOp::Mod if b != 0 => a.checked_rem(b),
27235                BinOp::Mod => return Some(Err("division by zero".into())),
27236                _ => return None,
27237            };
27238            // PG evaluates the arithmetic in the operand's own type, so an
27239            // int-by-int product that leaves int range fails there — before
27240            // the row count is ever looked at.
27241            match out {
27242                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27243                    Some(Err("integer out of range".into()))
27244                }
27245                Some(v) => Some(Ok(v)),
27246                None => Some(Err("integer out of range".into())),
27247            }
27248        }
27249        _ => None,
27250    }
27251}
27252
27253/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27254/// cast, which is what makes `LIMIT 2.5` keep three rows.
27255fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27256    if scale == 0 {
27257        return unscaled;
27258    }
27259    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27260        return 0;
27261    };
27262    let neg = unscaled < 0;
27263    let mag = unscaled.unsigned_abs() as i128;
27264    let rounded = (mag + div / 2) / div;
27265    if neg { -rounded } else { rounded }
27266}
27267
27268#[cfg(test)]
27269mod tests {
27270    use super::*;
27271    use alloc::string::ToString;
27272
27273    fn parse(s: &str) -> Statement {
27274        parse_statement(s).expect("parse ok")
27275    }
27276
27277    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27278    // `tables`, `partition`, etc. are unreserved keywords per PG's
27279    // `pg_get_keywords()` and MUST be usable as column / table /
27280    // alias names. Pre-T4 every drop-in user whose schema had one
27281    // of these as a column name (sentori events.release, mailrs
27282    // messages.index in some forks) blew the parser up at CREATE
27283    // TABLE time with "expected identifier, got Release". The
27284    // generalisation lives in `unreserved_keyword_text` + the
27285    // `expect_ident_like` and `parse_atom` arms that consult it.
27286    #[test]
27287    fn release_usable_as_column_name_in_create_table() {
27288        let stmt =
27289            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27290        if let Statement::CreateTable(t) = stmt {
27291            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27292            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27293        } else {
27294            panic!("expected CreateTable");
27295        }
27296    }
27297
27298    #[test]
27299    fn release_usable_as_column_ref_in_select_projection() {
27300        // The sentori `0003_partition_events.sql` INSERT-SELECT
27301        // walk references `release` in both column lists; the
27302        // projection-side use exercises `parse_atom`'s relaxed
27303        // identifier set.
27304        parse("SELECT id, release, payload FROM events WHERE id = 1");
27305    }
27306
27307    #[test]
27308    fn release_usable_as_column_ref_in_insert_column_list() {
27309        // INSERT INTO t (id, release, payload) VALUES (…)
27310        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27311    }
27312
27313    #[test]
27314    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27315        // Sentori `0013_audit_tombstone.sql` issues
27316        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27317        // emits Token::Drop (not Ident("drop")); the parser must
27318        // accept both in the ALTER COLUMN sub-dispatch.
27319        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27320    }
27321
27322    #[test]
27323    fn create_index_accepts_parenthesised_expression_key() {
27324        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27325        // expression index. Pre-T4 the parser bailed at the
27326        // inner `(` with "expected column ident or expression,
27327        // got LParen". The Token::LParen arm in CREATE INDEX
27328        // routes through the expression parser instead.
27329        parse(
27330            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27331             ON events ((payload->'bundle'->>'id'))",
27332        );
27333    }
27334
27335    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27336    // surface as parse errors, never stack overflows (embed hosts
27337    // abort on overflow).
27338    /// The nesting budget is a COUNT; what it has to fit inside is a
27339    /// number of BYTES, and only one of those two is stable across
27340    /// compiler versions. Round 847 measured 30,336 bytes per level
27341    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27342    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27343    /// aborted instead of erroring, which is precisely the outcome it
27344    /// exists to rule out.
27345    ///
27346    /// So the budget is metered rather than assumed. The ceiling leaves
27347    /// the depth SPG advertises fitting in a default 2 MiB thread with
27348    /// room to spare, in the debug build, where frames are widest.
27349    #[test]
27350    fn nesting_frame_cost_stays_under_ceiling() {
27351        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27352        // thread keeps a margin for whatever called the parser.
27353        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27354
27355        frame_meter::reset();
27356        let depth = frame_meter::SAMPLE_HI + 8;
27357        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27358        parse(&sql);
27359
27360        let per_level = frame_meter::bytes_per_level();
27361        {
27362            extern crate std;
27363            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27364        }
27365        assert!(
27366            per_level <= CEILING,
27367            "{per_level} bytes per nesting level exceeds {CEILING}; \
27368             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27369             in parse_expr_inner / parse_unary rather than lowering the \
27370             depth or widening the stack.",
27371            per_level * MAX_NEST_DEPTH
27372        );
27373    }
27374
27375    #[test]
27376    fn nesting_budget_errors_cleanly() {
27377        let depth = MAX_NEST_DEPTH + 50;
27378        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27379        let err = parse_statement(&sql).expect_err("must reject");
27380        assert!(err.message.contains("nests deeper"), "{err:?}");
27381        // Within budget still parses.
27382        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27383        parse(&sql);
27384    }
27385
27386    #[test]
27387    fn binary_chain_budget_errors_cleanly() {
27388        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27389        let err = parse_statement(&sql).expect_err("must reject");
27390        assert!(err.message.contains("chained binary"), "{err:?}");
27391        // Within budget still parses (chain depth ≤ budget is safe
27392        // for recursive eval/drop on 2 MiB stacks).
27393        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27394        parse(&sql);
27395    }
27396
27397    #[test]
27398    fn in_list_unaffected_by_chain_budget() {
27399        // Flat InList: 20k elements parse fine and stay flat.
27400        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27401        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27402        let Statement::Select(s) = parse(&sql) else {
27403            panic!("expected select")
27404        };
27405        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27406            panic!("expected flat InList, got {:?}", s.where_)
27407        };
27408        assert_eq!(list.len(), 20_000);
27409        assert!(!negated);
27410    }
27411
27412    fn lit_int(n: i64) -> Expr {
27413        Expr::Literal(Literal::Integer(n))
27414    }
27415
27416    fn col(name: &str) -> Expr {
27417        Expr::Column(ColumnName {
27418            qualifier: None,
27419            name: name.into(),
27420        })
27421    }
27422
27423    #[test]
27424    fn select_single_integer() {
27425        let s = parse("SELECT 1");
27426        let Statement::Select(s) = s else {
27427            panic!("expected SELECT")
27428        };
27429        assert_eq!(s.items.len(), 1);
27430        assert!(s.from.is_none());
27431        assert!(s.where_.is_none());
27432    }
27433
27434    #[test]
27435    fn select_multiple_literal_kinds() {
27436        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27437        let Statement::Select(s) = s else {
27438            panic!("expected SELECT")
27439        };
27440        assert_eq!(s.items.len(), 5);
27441    }
27442
27443    #[test]
27444    fn select_wildcard_from_table() {
27445        let s = parse("SELECT * FROM users");
27446        let Statement::Select(s) = s else {
27447            panic!("expected SELECT")
27448        };
27449        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27450        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27451    }
27452
27453    #[test]
27454    fn select_with_table_alias() {
27455        let s = parse("SELECT * FROM users AS u");
27456        let Statement::Select(s) = s else {
27457            panic!("expected SELECT")
27458        };
27459        let t = &s.from.as_ref().unwrap().primary;
27460        assert_eq!(t.name, "users");
27461        assert_eq!(t.alias.as_deref(), Some("u"));
27462    }
27463
27464    #[test]
27465    fn select_with_where_eq() {
27466        let s = parse("SELECT a FROM t WHERE a = 1");
27467        let Statement::Select(s) = s else {
27468            panic!("expected SELECT")
27469        };
27470        let w = s.where_.unwrap();
27471        assert_eq!(
27472            w,
27473            Expr::Binary {
27474                lhs: Box::new(col("a")),
27475                op: BinOp::Eq,
27476                rhs: Box::new(lit_int(1)),
27477            }
27478        );
27479    }
27480
27481    #[test]
27482    fn arithmetic_precedence() {
27483        let s = parse("SELECT 1 + 2 * 3");
27484        let Statement::Select(s) = s else {
27485            panic!("expected SELECT")
27486        };
27487        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27488            panic!("wildcard?")
27489        };
27490        assert_eq!(
27491            expr,
27492            &Expr::Binary {
27493                lhs: Box::new(lit_int(1)),
27494                op: BinOp::Add,
27495                rhs: Box::new(Expr::Binary {
27496                    lhs: Box::new(lit_int(2)),
27497                    op: BinOp::Mul,
27498                    rhs: Box::new(lit_int(3)),
27499                }),
27500            }
27501        );
27502    }
27503
27504    #[test]
27505    fn parentheses_override_precedence() {
27506        let s = parse("SELECT (1 + 2) * 3");
27507        let Statement::Select(s) = s else {
27508            panic!("expected SELECT")
27509        };
27510        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27511            panic!()
27512        };
27513        assert_eq!(
27514            expr,
27515            &Expr::Binary {
27516                lhs: Box::new(Expr::Binary {
27517                    lhs: Box::new(lit_int(1)),
27518                    op: BinOp::Add,
27519                    rhs: Box::new(lit_int(2)),
27520                }),
27521                op: BinOp::Mul,
27522                rhs: Box::new(lit_int(3)),
27523            }
27524        );
27525    }
27526
27527    #[test]
27528    fn not_binds_below_comparison() {
27529        // `NOT a = 1` should parse as `NOT (a = 1)`.
27530        let s = parse("SELECT NOT a = 1 FROM t");
27531        let Statement::Select(s) = s else {
27532            panic!("expected SELECT")
27533        };
27534        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27535            panic!()
27536        };
27537        assert_eq!(
27538            expr,
27539            &Expr::Unary {
27540                op: UnOp::Not,
27541                expr: Box::new(Expr::Binary {
27542                    lhs: Box::new(col("a")),
27543                    op: BinOp::Eq,
27544                    rhs: Box::new(lit_int(1)),
27545                }),
27546            }
27547        );
27548    }
27549
27550    #[test]
27551    fn unary_minus_binds_above_multiplication() {
27552        // `-a * 2` should be `(-a) * 2`.
27553        let s = parse("SELECT -a * 2 FROM t");
27554        let Statement::Select(s) = s else {
27555            panic!("expected SELECT")
27556        };
27557        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27558            panic!()
27559        };
27560        assert_eq!(
27561            expr,
27562            &Expr::Binary {
27563                lhs: Box::new(Expr::Unary {
27564                    op: UnOp::Neg,
27565                    expr: Box::new(col("a")),
27566                }),
27567                op: BinOp::Mul,
27568                rhs: Box::new(lit_int(2)),
27569            }
27570        );
27571    }
27572
27573    #[test]
27574    fn qualified_column() {
27575        let s = parse("SELECT t.col FROM t");
27576        let Statement::Select(s) = s else {
27577            panic!("expected SELECT")
27578        };
27579        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27580            panic!()
27581        };
27582        assert_eq!(
27583            expr,
27584            &Expr::Column(ColumnName {
27585                qualifier: Some("t".into()),
27586                name: "col".into()
27587            })
27588        );
27589    }
27590
27591    #[test]
27592    fn select_item_alias_with_as() {
27593        let s = parse("SELECT a AS y FROM t");
27594        let Statement::Select(s) = s else {
27595            panic!("expected SELECT")
27596        };
27597        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27598            panic!()
27599        };
27600        assert_eq!(alias.as_deref(), Some("y"));
27601    }
27602
27603    #[test]
27604    fn trailing_semicolon_accepted() {
27605        let s = parse("SELECT 1;");
27606        let Statement::Select(s) = s else {
27607            panic!("expected SELECT")
27608        };
27609        assert_eq!(s.items.len(), 1);
27610    }
27611
27612    #[test]
27613    fn boolean_chain_with_and_or_not() {
27614        // (NOT a) OR (b AND (NOT c))
27615        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27616        let Statement::Select(s) = s else {
27617            panic!("expected SELECT")
27618        };
27619        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27620            panic!()
27621        };
27622        let expected = Expr::Binary {
27623            lhs: Box::new(Expr::Unary {
27624                op: UnOp::Not,
27625                expr: Box::new(col("a")),
27626            }),
27627            op: BinOp::Or,
27628            rhs: Box::new(Expr::Binary {
27629                lhs: Box::new(col("b")),
27630                op: BinOp::And,
27631                rhs: Box::new(Expr::Unary {
27632                    op: UnOp::Not,
27633                    expr: Box::new(col("c")),
27634                }),
27635            }),
27636        };
27637        assert_eq!(expr, &expected);
27638    }
27639
27640    #[test]
27641    fn empty_input_errors() {
27642        // v7.14.0 — pg_dump preambles emit several comment-only
27643        // / blank-line statements that collapse to Statement::
27644        // Empty rather than a parse error. The old "SELECT in
27645        // message" assertion is stale; verify the new contract:
27646        // empty / whitespace / comment-only input parses to
27647        // Statement::Empty.
27648        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27649        assert!(matches!(
27650            parse_statement("  \n\t ").unwrap(),
27651            Statement::Empty
27652        ));
27653        // Sanity: malformed-but-non-empty still errors.
27654        assert!(parse_statement("SELECT FROM WHERE").is_err());
27655    }
27656
27657    #[test]
27658    fn unmatched_paren_errors() {
27659        assert!(parse_statement("SELECT (1 + 2").is_err());
27660    }
27661
27662    #[test]
27663    fn display_round_trip_simple_select() {
27664        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27665        let text = original.to_string();
27666        let again = parse_statement(&text).expect("re-parse");
27667        assert_eq!(original, again);
27668    }
27669
27670    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27671
27672    #[test]
27673    fn create_table_single_column() {
27674        let s = parse("CREATE TABLE foo (a INT)");
27675        let Statement::CreateTable(c) = s else {
27676            panic!("expected CreateTable")
27677        };
27678        assert_eq!(c.name, "foo");
27679        assert_eq!(c.columns.len(), 1);
27680        assert_eq!(c.columns[0].name, "a");
27681        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27682        assert!(c.columns[0].nullable);
27683    }
27684
27685    #[test]
27686    fn create_table_multi_column_with_not_null_mix() {
27687        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27688        let Statement::CreateTable(c) = s else {
27689            panic!()
27690        };
27691        assert_eq!(c.columns.len(), 4);
27692        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27693        assert!(!c.columns[0].nullable);
27694        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27695        assert!(c.columns[1].nullable);
27696        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27697        assert!(!c.columns[2].nullable);
27698        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27699    }
27700
27701    #[test]
27702    fn create_table_bigint_supported() {
27703        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27704        let Statement::CreateTable(c) = s else {
27705            panic!()
27706        };
27707        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27708    }
27709
27710    #[test]
27711    fn create_table_vector_default_is_f32() {
27712        let s = parse("CREATE TABLE t (v VECTOR(128))");
27713        let Statement::CreateTable(c) = s else {
27714            panic!()
27715        };
27716        assert_eq!(
27717            c.columns[0].ty,
27718            ColumnTypeName::Vector {
27719                dim: 128,
27720                encoding: VecEncoding::F32,
27721            },
27722        );
27723    }
27724
27725    #[test]
27726    fn create_table_vector_using_sq8() {
27727        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27728        // Case-insensitive on both `USING` and the encoding name.
27729        for sql in [
27730            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27731            "CREATE TABLE t (v VECTOR(128) using sq8)",
27732        ] {
27733            let s = parse(sql);
27734            let Statement::CreateTable(c) = s else {
27735                panic!()
27736            };
27737            assert_eq!(
27738                c.columns[0].ty,
27739                ColumnTypeName::Vector {
27740                    dim: 128,
27741                    encoding: VecEncoding::Sq8,
27742                },
27743                "{sql}",
27744            );
27745        }
27746    }
27747
27748    #[test]
27749    fn create_table_vector_using_unknown_errors() {
27750        // v7.16.1 — the inline `USING <encoding>` shape on
27751        // CREATE TABLE column defs was withdrawn before
27752        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27753        // (col vector_<metric>_ops)`; the parser now rejects
27754        // USING at column-list position with a clearer
27755        // "expected ',' or ')'" message. Test asserts the
27756        // current rejection, not the old "unknown vector
27757        // encoding" string.
27758        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27759        assert!(
27760            err.message.contains("USING")
27761                || err.message.contains("using")
27762                || err.message.contains("')'")
27763                || err.message.contains("','"),
27764            "expected USING/column-list rejection, got: {}",
27765            err.message
27766        );
27767    }
27768
27769    #[test]
27770    fn vector_using_sq8_display_roundtrips() {
27771        // The Display impl must produce text that re-parses to the
27772        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27773        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27774        let Statement::CreateTable(c) = s else {
27775            panic!()
27776        };
27777        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27778    }
27779
27780    #[test]
27781    fn parser_recognises_placeholders() {
27782        use crate::ast::{Expr, SelectItem, Statement};
27783        // $N in expression position parses as Expr::Placeholder(N).
27784        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27785        let Statement::Select(sel) = s else { panic!() };
27786        assert!(matches!(
27787            sel.items[0],
27788            SelectItem::Expr {
27789                expr: Expr::Placeholder(1),
27790                alias: None
27791            }
27792        ));
27793        // $2 + 1
27794        let SelectItem::Expr {
27795            expr: Expr::Binary { lhs, rhs, .. },
27796            ..
27797        } = &sel.items[1]
27798        else {
27799            panic!()
27800        };
27801        assert!(matches!(**lhs, Expr::Placeholder(2)));
27802        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27803        // WHERE x = $3
27804        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27805            panic!()
27806        };
27807        assert!(matches!(**rhs, Expr::Placeholder(3)));
27808    }
27809
27810    #[test]
27811    fn parser_rejects_dollar_zero() {
27812        // $0 is not valid in PG; the lexer rejects it.
27813        assert!(parse_statement("SELECT $0").is_err());
27814    }
27815
27816    #[test]
27817    fn placeholder_display_roundtrips() {
27818        // The Display impl must produce text that re-lexes to the
27819        // same Placeholder token.
27820        let s = parse("SELECT $42 FROM t");
27821        let printed = s.to_string();
27822        assert!(printed.contains("$42"));
27823        let again = parse(&printed);
27824        assert_eq!(s, again);
27825    }
27826
27827    #[test]
27828    fn alter_index_rebuild_bare() {
27829        use crate::ast::{AlterIndexTarget, Statement};
27830        let s = parse("ALTER INDEX my_idx REBUILD");
27831        let Statement::AlterIndex(a) = s else {
27832            panic!("expected AlterIndex, got {s:?}")
27833        };
27834        assert_eq!(a.name, "my_idx");
27835        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27836    }
27837
27838    #[test]
27839    fn alter_index_rebuild_with_encoding() {
27840        use crate::ast::{AlterIndexTarget, Statement};
27841        for (sql, want) in [
27842            (
27843                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27844                VecEncoding::F32,
27845            ),
27846            (
27847                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27848                VecEncoding::Sq8,
27849            ),
27850            (
27851                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27852                VecEncoding::F16,
27853            ),
27854        ] {
27855            let s = parse(sql);
27856            let Statement::AlterIndex(a) = s else {
27857                panic!("{sql}: expected AlterIndex")
27858            };
27859            assert_eq!(a.name, "my_idx");
27860            assert_eq!(
27861                a.target,
27862                AlterIndexTarget::Rebuild {
27863                    encoding: Some(want)
27864                },
27865                "{sql}"
27866            );
27867        }
27868    }
27869
27870    #[test]
27871    fn alter_index_rebuild_unknown_encoding_errors() {
27872        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27873        assert!(
27874            err.message.contains("unknown vector encoding"),
27875            "got: {}",
27876            err.message
27877        );
27878    }
27879
27880    #[test]
27881    fn alter_index_rebuild_display_roundtrips() {
27882        for (input, want) in [
27883            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27884            (
27885                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27886                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27887            ),
27888            (
27889                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27890                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27891            ),
27892        ] {
27893            let s = parse(input);
27894            assert_eq!(s.to_string(), want);
27895        }
27896    }
27897
27898    #[test]
27899    fn create_table_unknown_type_defers_to_engine() {
27900        // v4.9 picked XML as a parse-time "unsupported column
27901        // type" probe. v7.17.0 Phase 1.4 changed the contract:
27902        // an unknown type ident parses as Text + `user_type_ref`
27903        // so CREATE TABLE can resolve user-defined enum / domain
27904        // types — rejection of truly-unknown types moved to the
27905        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27906        // to a first-class built-in, so this probe switched to a
27907        // synthetic name nothing in the lexer will ever recognise.
27908        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27909        let Statement::CreateTable(t) = stmt else {
27910            panic!("expected CreateTable");
27911        };
27912        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27913    }
27914
27915    #[test]
27916    fn create_table_missing_table_keyword_errors() {
27917        assert!(parse_statement("CREATE x (a INT)").is_err());
27918    }
27919
27920    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27921    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27922
27923    #[test]
27924    fn parse_create_table_partition_by_range() {
27925        use crate::ast::{PartitionBySpec, PartitionKindAst};
27926        let stmt = parse_statement(
27927            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27928             payload JSONB) PARTITION BY RANGE (ts)",
27929        )
27930        .unwrap();
27931        let Statement::CreateTable(t) = stmt else {
27932            panic!("expected CreateTable");
27933        };
27934        assert!(t.partition_of.is_none(), "parent has no partition_of");
27935        assert_eq!(t.columns.len(), 3);
27936        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27937        assert_eq!(
27938            by,
27939            &PartitionBySpec {
27940                kind: PartitionKindAst::Range,
27941                key_columns: alloc::vec!["ts".to_string()],
27942            }
27943        );
27944        // Display round-trip preserves the suffix. `quote_ident`
27945        // only adds double quotes when the ident needs escaping, so
27946        // a plain `ts` survives bare here.
27947        assert!(
27948            t.to_string().contains("PARTITION BY RANGE (ts)"),
27949            "Display lost PARTITION BY suffix: {t}"
27950        );
27951    }
27952
27953    #[test]
27954    fn parse_create_table_partition_of_range() {
27955        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27956        let stmt = parse_statement(
27957            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27958             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27959        )
27960        .unwrap();
27961        let Statement::CreateTable(t) = stmt else {
27962            panic!("expected CreateTable");
27963        };
27964        assert!(t.columns.is_empty(), "child inherits columns from parent");
27965        assert!(t.partition_by.is_none());
27966        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27967        assert_eq!(of.parent_name, "events_partitioned");
27968        let PartitionOfSpec { bounds, .. } = of.clone();
27969        match bounds {
27970            PartitionOfBoundsAst::Range { lower, upper } => {
27971                assert!(lower.to_string().contains("2026-06-01"));
27972                assert!(upper.to_string().contains("2026-07-01"));
27973            }
27974            other => panic!("expected Range, got {other:?}"),
27975        }
27976        // Display round-trip emits the FOR VALUES tail. `quote_ident`
27977        // skips quotes when not required, so the parent name appears
27978        // bare here.
27979        let s = t.to_string();
27980        assert!(
27981            s.contains("PARTITION OF events_partitioned"),
27982            "Display lost PARTITION OF: {s}"
27983        );
27984        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27985        assert!(s.contains(") TO ("), "Display lost TO: {s}");
27986    }
27987
27988    #[test]
27989    fn parse_create_table_partition_of_default() {
27990        use crate::ast::PartitionOfBoundsAst;
27991        let stmt =
27992            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27993                .unwrap();
27994        let Statement::CreateTable(t) = stmt else {
27995            panic!("expected CreateTable");
27996        };
27997        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27998        assert_eq!(of.parent_name, "events_partitioned");
27999        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28000        assert!(
28001            t.to_string()
28002                .contains("PARTITION OF events_partitioned DEFAULT"),
28003            "Display lost DEFAULT: {t}"
28004        );
28005    }
28006
28007    #[test]
28008    fn parse_create_table_partition_by_list() {
28009        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28010        // child with `FOR VALUES IN (lit, lit, …)`.
28011        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28012        let parent =
28013            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28014                .unwrap();
28015        let Statement::CreateTable(t) = parent else {
28016            panic!("expected CreateTable");
28017        };
28018        let Some(PartitionBySpec {
28019            kind,
28020            ref key_columns,
28021        }) = t.partition_by
28022        else {
28023            panic!("expected PARTITION BY");
28024        };
28025        assert_eq!(kind, PartitionKindAst::List);
28026        assert_eq!(*key_columns, vec!["region".to_string()]);
28027        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28028
28029        let child = parse_statement(
28030            "CREATE TABLE events_apac PARTITION OF events_listed \
28031             FOR VALUES IN ('jp', 'kr', 'tw')",
28032        )
28033        .unwrap();
28034        let Statement::CreateTable(c) = child else {
28035            panic!("expected CreateTable");
28036        };
28037        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28038        let PartitionOfBoundsAst::List { values } = &of.bounds else {
28039            panic!("expected List bounds, got {:?}", of.bounds);
28040        };
28041        assert_eq!(values.len(), 3);
28042        let disp = c.to_string();
28043        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28044    }
28045
28046    #[test]
28047    fn parse_create_table_partition_by_hash() {
28048        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28049        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28050        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28051        let parent =
28052            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28053        let Statement::CreateTable(t) = parent else {
28054            panic!("expected CreateTable");
28055        };
28056        let Some(PartitionBySpec {
28057            kind,
28058            ref key_columns,
28059        }) = t.partition_by
28060        else {
28061            panic!("expected PARTITION BY");
28062        };
28063        assert_eq!(kind, PartitionKindAst::Hash);
28064        assert_eq!(*key_columns, vec!["id".to_string()]);
28065        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28066
28067        let child = parse_statement(
28068            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28069             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28070        )
28071        .unwrap();
28072        let Statement::CreateTable(c) = child else {
28073            panic!("expected CreateTable");
28074        };
28075        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28076        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28077            panic!("expected Hash bounds");
28078        };
28079        assert_eq!(modulus, 4);
28080        assert_eq!(remainder, 0);
28081        let disp = c.to_string();
28082        assert!(
28083            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28084            "Display lost HASH bounds: {disp}"
28085        );
28086
28087        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28088        let bad = parse_statement(
28089            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28090             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28091        );
28092        let msg = format!("{}", bad.unwrap_err());
28093        assert!(
28094            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28095            "expected REMAINDER/MODULUS validation error: {msg}"
28096        );
28097    }
28098
28099    #[test]
28100    fn parse_create_table_partition_of_rejects_columns() {
28101        // v7.37.6-B contract: PARTITION OF children inherit columns
28102        // from the parent; an explicit list MUST surface as a parse
28103        // error rather than getting silently ignored.
28104        let err = parse_statement(
28105            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28106             FOR VALUES FROM ('a') TO ('b')",
28107        );
28108        assert!(err.is_err(), "expected parse error for explicit columns");
28109        let msg = format!("{}", err.unwrap_err());
28110        assert!(
28111            msg.contains("PARTITION OF") && msg.contains("column"),
28112            "error should mention PARTITION OF + columns: {msg}"
28113        );
28114    }
28115
28116    #[test]
28117    fn insert_single_value() {
28118        let s = parse("INSERT INTO foo VALUES (42)");
28119        let Statement::Insert(i) = s else {
28120            panic!("expected Insert")
28121        };
28122        assert_eq!(i.table, "foo");
28123        assert_eq!(i.rows.len(), 1);
28124        assert_eq!(i.rows[0].len(), 1);
28125        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28126    }
28127
28128    #[test]
28129    fn insert_multi_value_with_mixed_literals() {
28130        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28131        let Statement::Insert(i) = s else { panic!() };
28132        assert_eq!(i.rows.len(), 1);
28133        assert_eq!(i.rows[0].len(), 5);
28134    }
28135
28136    #[test]
28137    fn insert_missing_into_errors() {
28138        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28139    }
28140
28141    #[test]
28142    fn create_table_round_trip() {
28143        let original =
28144            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28145        let text = original.to_string();
28146        let again = parse_statement(&text).expect("re-parse");
28147        assert_eq!(original, again);
28148    }
28149
28150    #[test]
28151    fn insert_round_trip_with_negation_and_string() {
28152        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28153        let text = original.to_string();
28154        let again = parse_statement(&text).expect("re-parse");
28155        assert_eq!(original, again);
28156    }
28157
28158    #[test]
28159    fn unknown_keyword_at_statement_start_errors() {
28160        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28161        // the top-level dispatch still has no branch to take.
28162        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28163        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28164    }
28165
28166    // --- v0.8 CREATE INDEX --------------------------------------------------
28167
28168    #[test]
28169    fn create_index_basic() {
28170        let s = parse("CREATE INDEX idx_id ON users (id)");
28171        let Statement::CreateIndex(c) = s else {
28172            panic!("expected CreateIndex")
28173        };
28174        assert_eq!(c.name, "idx_id");
28175        assert_eq!(c.table, "users");
28176        assert_eq!(c.column, "id");
28177    }
28178
28179    #[test]
28180    fn create_index_missing_on_errors() {
28181        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28182    }
28183
28184    #[test]
28185    fn create_index_missing_paren_errors() {
28186        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28187    }
28188
28189    #[test]
28190    fn create_index_round_trip() {
28191        let original = parse("CREATE INDEX by_name ON users (name)");
28192        let again = parse_statement(&original.to_string()).unwrap();
28193        assert_eq!(original, again);
28194    }
28195
28196    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28197
28198    #[test]
28199    fn create_unique_index_basic() {
28200        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28201        let Statement::CreateIndex(c) = s else {
28202            panic!("expected CreateIndex");
28203        };
28204        assert!(c.is_unique);
28205        assert_eq!(c.column, "a");
28206        assert!(c.partial_predicate.is_none());
28207    }
28208
28209    #[test]
28210    fn create_unique_index_partial() {
28211        // mailrs's email_templates "one default per user" shape.
28212        let s = parse(
28213            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28214             ON email_templates (user_address) WHERE is_default = true",
28215        );
28216        let Statement::CreateIndex(c) = s else {
28217            panic!("expected CreateIndex");
28218        };
28219        assert!(c.is_unique);
28220        assert_eq!(c.table, "email_templates");
28221        assert_eq!(c.column, "user_address");
28222        assert!(c.partial_predicate.is_some());
28223    }
28224
28225    #[test]
28226    fn create_unique_index_composite_with_predicate() {
28227        // mailrs's calendar_events instance: composite columns.
28228        let s = parse(
28229            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28230             ON calendar_events (calendar_id, uid, recurrence_id) \
28231             WHERE recurrence_id IS NOT NULL",
28232        );
28233        let Statement::CreateIndex(c) = s else {
28234            panic!("expected CreateIndex");
28235        };
28236        assert!(c.is_unique);
28237        assert_eq!(c.column, "calendar_id");
28238        assert_eq!(
28239            c.extra_columns,
28240            vec!["uid".to_string(), "recurrence_id".to_string()]
28241        );
28242        assert!(c.partial_predicate.is_some());
28243    }
28244
28245    #[test]
28246    fn create_unique_index_using_btree_ok() {
28247        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28248        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28249    }
28250
28251    #[test]
28252    fn create_unique_index_using_hnsw_rejected() {
28253        let err =
28254            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28255        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28256    }
28257
28258    #[test]
28259    fn create_unique_index_round_trip() {
28260        let original = parse(
28261            "CREATE UNIQUE INDEX uq_calendar_events_master \
28262             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28263        );
28264        let again = parse_statement(&original.to_string()).unwrap();
28265        assert_eq!(original, again);
28266    }
28267
28268    #[test]
28269    fn create_unique_without_index_errors() {
28270        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28271        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28272        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28273    }
28274
28275    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28276
28277    #[test]
28278    fn create_table_bytea_column() {
28279        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28280        let Statement::CreateTable(c) = s else {
28281            panic!("expected CreateTable");
28282        };
28283        assert_eq!(c.columns.len(), 2);
28284        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28285        assert!(!c.columns[1].nullable);
28286    }
28287
28288    #[test]
28289    fn create_table_bytes_alias_column() {
28290        let s = parse("CREATE TABLE t (blob BYTES)");
28291        let Statement::CreateTable(c) = s else {
28292            panic!("expected CreateTable");
28293        };
28294        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28295    }
28296
28297    #[test]
28298    fn bytea_round_trip_display() {
28299        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28300        let again = parse_statement(&original.to_string()).unwrap();
28301        assert_eq!(original, again);
28302    }
28303
28304    // --- v0.9 transactions -------------------------------------------------
28305
28306    #[test]
28307    fn begin_commit_rollback_parse_as_unit_variants() {
28308        assert_eq!(parse("BEGIN"), Statement::Begin(None));
28309        assert_eq!(parse("COMMIT"), Statement::Commit);
28310        // r1066 — PG synonyms pgbench's tpcb script relies on.
28311        assert_eq!(parse("END"), Statement::Commit);
28312        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28313        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28314        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28315        // Trailing semicolons accepted too.
28316        assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28317        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28318        // statement (with or without the WORK/TRANSACTION noise word).
28319        assert_eq!(
28320            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28321            Statement::Begin(Some(IsolationLevel::RepeatableRead))
28322        );
28323        assert_eq!(
28324            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28325            Statement::Begin(Some(IsolationLevel::Serializable))
28326        );
28327        // A non-isolation mode keeps the session default (None).
28328        assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28329    }
28330
28331    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28332
28333    #[test]
28334    fn inner_product_binop_parses() {
28335        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28336        let Statement::Select(s) = s else { panic!() };
28337        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28338            panic!()
28339        };
28340        assert!(matches!(
28341            expr,
28342            Expr::Binary {
28343                op: BinOp::InnerProduct,
28344                ..
28345            }
28346        ));
28347    }
28348
28349    #[test]
28350    fn cosine_distance_binop_parses() {
28351        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28352        let Statement::Select(s) = s else { panic!() };
28353        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28354            panic!()
28355        };
28356        assert!(matches!(
28357            expr,
28358            Expr::Binary {
28359                op: BinOp::CosineDistance,
28360                ..
28361            }
28362        ));
28363    }
28364
28365    #[test]
28366    fn vector_cast_postfix_wraps_string_literal() {
28367        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28368        let Statement::Select(s) = s else { panic!() };
28369        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28370            panic!()
28371        };
28372        assert!(matches!(
28373            expr,
28374            Expr::Cast {
28375                target: CastTarget::Vector,
28376                ..
28377            }
28378        ));
28379    }
28380
28381    #[test]
28382    fn unsupported_cast_target_errors() {
28383        // v7.37.5 ship triage promoted the parser to accept every
28384        // ident as a `CastTarget::Named(canonical)`; the engine
28385        // surfaces the "unsupported cast target" error at eval
28386        // time when `type_name_to_data_type` can't resolve it.
28387        // Parser-side error now requires a NON-ident after `::`
28388        // (e.g. a punctuation token).
28389        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28390        assert_eq!(err.message, "syntax error at or near \",\"");
28391    }
28392
28393    #[test]
28394    fn tx_statements_round_trip() {
28395        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28396            let original = parse(q);
28397            let again = parse_statement(&original.to_string()).unwrap();
28398            assert_eq!(original, again);
28399        }
28400    }
28401
28402    #[test]
28403    fn interval_text_parsing_units() {
28404        // v7.37.5 β — three-field shape `(months, days, micros)` so
28405        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28406        // Single unit.
28407        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28408        assert_eq!(
28409            parse_interval_text("24 hours"),
28410            Some((0, 0, 86_400_000_000))
28411        );
28412        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28413        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28414        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28415        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28416        // Compound spans accumulate per-dimension.
28417        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28418        assert_eq!(
28419            parse_interval_text("1 day 2 hours"),
28420            Some((0, 1, 7_200_000_000))
28421        );
28422        // Negative numbers carry through per-dimension.
28423        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28424        // Bad shapes return None.
28425        assert_eq!(parse_interval_text(""), None);
28426        assert_eq!(parse_interval_text("garbage"), None);
28427        assert_eq!(parse_interval_text("1 fortnight"), None);
28428        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28429        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28430        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28431        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28432        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28433    }
28434
28435    #[test]
28436    fn interval_literal_roundtrips_via_display() {
28437        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28438        let s = parsed.to_string();
28439        // Display preserves the original text verbatim.
28440        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28441        // And re-parsing yields a structurally equal statement.
28442        let again = parse_statement(&s).unwrap();
28443        assert_eq!(parsed, again);
28444    }
28445
28446    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28447
28448    #[test]
28449    fn parser_recognises_create_publication_bare() {
28450        let s = parse("CREATE PUBLICATION pub_a");
28451        let Statement::CreatePublication(p) = s else {
28452            panic!("expected CreatePublication, got {s:?}")
28453        };
28454        assert_eq!(p.name, "pub_a");
28455        assert_eq!(p.scope, PublicationScope::AllTables);
28456    }
28457
28458    #[test]
28459    fn parser_recognises_create_publication_for_all_tables() {
28460        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28461        let Statement::CreatePublication(p) = s else {
28462            panic!("expected CreatePublication, got {s:?}")
28463        };
28464        assert_eq!(p.name, "pub_a");
28465        assert_eq!(p.scope, PublicationScope::AllTables);
28466    }
28467
28468    #[test]
28469    fn parser_recognises_drop_publication() {
28470        let s = parse("DROP PUBLICATION pub_a");
28471        let Statement::DropPublication { name, .. } = s else {
28472            panic!("expected DropPublication, got {s:?}")
28473        };
28474        assert_eq!(name, "pub_a");
28475    }
28476
28477    #[test]
28478    fn parser_recognises_for_table_list() {
28479        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28480        let Statement::CreatePublication(p) = s else {
28481            panic!("expected CreatePublication, got {s:?}")
28482        };
28483        assert_eq!(p.name, "pub_a");
28484        let PublicationScope::ForTables(ts) = p.scope else {
28485            panic!("expected ForTables scope")
28486        };
28487        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28488    }
28489
28490    #[test]
28491    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28492        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28493        // is rejected (`invalid publication object list`; the old
28494        // test pinned an unverifiable "PG 19 accepts both" claim);
28495        // TABLES pairs with IN SCHEMA.
28496        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28497            .expect_err("bare FOR TABLES must reject");
28498        assert!(
28499            alloc::format!("{err}").contains("invalid publication object list"),
28500            "got: {err}"
28501        );
28502        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28503        let Statement::CreatePublication(p) = s else {
28504            panic!("expected CreatePublication, got {s:?}")
28505        };
28506        let PublicationScope::TablesInSchema(schema) = p.scope else {
28507            panic!("expected TablesInSchema")
28508        };
28509        assert_eq!(schema, "public");
28510    }
28511
28512    #[test]
28513    fn parser_recognises_for_all_tables_except_list() {
28514        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28515        let Statement::CreatePublication(p) = s else {
28516            panic!()
28517        };
28518        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28519            panic!("expected AllTablesExcept")
28520        };
28521        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28522    }
28523
28524    #[test]
28525    fn parser_rejects_for_table_with_empty_list() {
28526        // `FOR TABLE` with nothing after is a parse error.
28527        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28528            .expect_err("must error on empty list");
28529        // No specific message asserted — the call falls through to
28530        // expect_ident_like which yields "expected identifier, got …".
28531        assert!(!err.message.is_empty());
28532    }
28533
28534    #[test]
28535    fn parser_recognises_show_publications() {
28536        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28537        // bare ident in this position, NOT a reserved keyword.
28538        let s = parse("SHOW PUBLICATIONS");
28539        assert!(matches!(s, Statement::ShowPublications));
28540    }
28541
28542    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28543
28544    #[test]
28545    fn parser_recognises_create_subscription_single_publication() {
28546        let s = parse(
28547            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28548        );
28549        let Statement::CreateSubscription(c) = s else {
28550            panic!("expected CreateSubscription, got {s:?}")
28551        };
28552        assert_eq!(c.name, "sub_a");
28553        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28554        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28555    }
28556
28557    #[test]
28558    fn parser_recognises_create_subscription_multi_publication() {
28559        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28560        let Statement::CreateSubscription(c) = s else {
28561            panic!()
28562        };
28563        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28564    }
28565
28566    #[test]
28567    fn parser_rejects_create_subscription_missing_connection() {
28568        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28569            .expect_err("must error on missing CONNECTION");
28570        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28571    }
28572
28573    #[test]
28574    fn parser_rejects_create_subscription_missing_publication() {
28575        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28576            .expect_err("must error on missing PUBLICATION");
28577        assert_eq!(err.message, "syntax error at end of input");
28578    }
28579
28580    #[test]
28581    fn parser_recognises_drop_subscription() {
28582        let s = parse("DROP SUBSCRIPTION sub_a");
28583        let Statement::DropSubscription { name, .. } = s else {
28584            panic!("expected DropSubscription, got {s:?}")
28585        };
28586        assert_eq!(name, "sub_a");
28587    }
28588
28589    #[test]
28590    fn parser_recognises_show_subscriptions() {
28591        let s = parse("SHOW SUBSCRIPTIONS");
28592        assert!(matches!(s, Statement::ShowSubscriptions));
28593    }
28594
28595    #[test]
28596    fn parser_recognises_wait_for_wal_position_no_timeout() {
28597        let s = parse("WAIT FOR WAL POSITION 12345");
28598        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28599            panic!("expected WaitForWalPosition, got {s:?}")
28600        };
28601        assert_eq!(pos, 12345);
28602        assert!(timeout_ms.is_none());
28603    }
28604
28605    #[test]
28606    fn parser_recognises_wait_for_wal_position_with_timeout() {
28607        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28608        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28609            panic!()
28610        };
28611        assert_eq!(pos, 67890);
28612        assert_eq!(timeout_ms, Some(5000));
28613    }
28614
28615    #[test]
28616    fn parser_rejects_wait_with_negative_position() {
28617        // The lexer treats `-` as a token; `expect_u64_literal`
28618        // only sees the Integer that follows, so the negative
28619        // arrives as a unary-minus expression at higher levels.
28620        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28621        // parse error one way or another.
28622        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28623        assert!(!err.message.is_empty());
28624    }
28625
28626    #[test]
28627    fn parser_recognises_bare_analyze() {
28628        let s = parse("ANALYZE");
28629        assert!(matches!(s, Statement::Analyze(None)));
28630    }
28631
28632    #[test]
28633    fn parser_recognises_analyze_with_table() {
28634        let s = parse("ANALYZE users");
28635        let Statement::Analyze(Some(name)) = s else {
28636            panic!("expected Analyze, got {s:?}")
28637        };
28638        assert_eq!(name, "users");
28639    }
28640
28641    #[test]
28642    fn parser_recognises_analyze_with_quoted_table() {
28643        let s = parse("ANALYZE \"Mixed Case\"");
28644        let Statement::Analyze(Some(name)) = s else {
28645            panic!()
28646        };
28647        assert_eq!(name, "Mixed Case");
28648    }
28649
28650    #[test]
28651    fn parser_rejects_analyze_with_garbage_token() {
28652        let err = parse_statement("ANALYZE 42").expect_err("must error");
28653        assert!(!err.message.is_empty());
28654    }
28655
28656    #[test]
28657    fn analyze_display_roundtrips() {
28658        for sql in ["ANALYZE", "ANALYZE users"] {
28659            let s = parse(sql);
28660            let printed = s.to_string();
28661            let again = parse_statement(&printed)
28662                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28663            assert_eq!(s, again);
28664        }
28665    }
28666
28667    #[test]
28668    fn wait_for_display_roundtrips() {
28669        for sql in [
28670            "WAIT FOR WAL POSITION 12345",
28671            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28672        ] {
28673            let s = parse(sql);
28674            let printed = s.to_string();
28675            let again = parse_statement(&printed)
28676                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28677            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28678        }
28679    }
28680
28681    #[test]
28682    fn subscription_ddl_display_roundtrips() {
28683        for sql in [
28684            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28685            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28686            "DROP SUBSCRIPTION sub_a",
28687            "SHOW SUBSCRIPTIONS",
28688        ] {
28689            let s = parse(sql);
28690            let printed = s.to_string();
28691            let again = parse_statement(&printed)
28692                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28693            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28694        }
28695    }
28696
28697    #[test]
28698    fn parser_drop_dispatches_user_vs_publication() {
28699        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28700        // tokenises DROP. Both targets must still parse.
28701        let s = parse("DROP USER 'alice'");
28702        let Statement::DropUser { name, .. } = s else {
28703            panic!("expected DropUser, got {s:?}")
28704        };
28705        assert_eq!(name, "alice");
28706        // And DROP PUBLICATION lands the new variant.
28707        let s = parse("DROP PUBLICATION p1");
28708        assert!(matches!(s, Statement::DropPublication { .. }));
28709    }
28710
28711    #[test]
28712    fn publication_ddl_display_roundtrips() {
28713        // Every CREATE PUBLICATION variant must Display → parse →
28714        // same AST. v6.1.3 covers all three scope shapes.
28715        for sql in [
28716            "CREATE PUBLICATION pub_a",
28717            "CREATE PUBLICATION pub_a FOR ALL TABLES",
28718            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28719            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28720            "DROP PUBLICATION pub_a",
28721            "SHOW PUBLICATIONS",
28722        ] {
28723            let s = parse(sql);
28724            let printed = s.to_string();
28725            let again = parse_statement(&printed)
28726                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28727            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28728        }
28729    }
28730
28731    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28732
28733    #[test]
28734    fn create_function_returns_trigger_plpgsql_minimal() {
28735        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28736        let s = parse(sql);
28737        let Statement::CreateFunction(f) = s else {
28738            panic!("expected CreateFunction");
28739        };
28740        assert_eq!(f.name, "noop");
28741        assert!(!f.or_replace);
28742        assert!(f.args.is_empty());
28743        assert!(matches!(f.returns, FunctionReturn::Trigger));
28744        assert_eq!(f.language, "plpgsql");
28745        let FunctionBody::PlPgSql(block) = f.body else {
28746            panic!("expected PlPgSql body");
28747        };
28748        assert_eq!(block.statements.len(), 1);
28749        assert!(matches!(
28750            block.statements[0],
28751            PlPgSqlStmt::Return(ReturnTarget::New)
28752        ));
28753    }
28754
28755    #[test]
28756    fn create_function_or_replace_with_assignment() {
28757        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28758        // RETURN NEW.
28759        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28760BEGIN
28761  NEW.search_vector := to_tsvector('english', NEW.subject);
28762  RETURN NEW;
28763END;
28764$$";
28765        let s = parse(sql);
28766        let Statement::CreateFunction(f) = s else {
28767            panic!("expected CreateFunction");
28768        };
28769        assert!(f.or_replace);
28770        let FunctionBody::PlPgSql(block) = &f.body else {
28771            panic!("expected PlPgSql body");
28772        };
28773        assert_eq!(block.statements.len(), 2);
28774        // First statement: NEW.search_vector := to_tsvector(...)
28775        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28776            panic!("expected Assign as first stmt");
28777        };
28778        match target {
28779            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28780            other => panic!("expected NEW.col, got {other:?}"),
28781        }
28782        // Second statement: RETURN NEW
28783        assert!(matches!(
28784            block.statements[1],
28785            PlPgSqlStmt::Return(ReturnTarget::New)
28786        ));
28787    }
28788
28789    #[test]
28790    fn create_trigger_after_insert_or_update() {
28791        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28792        let s = parse(sql);
28793        let Statement::CreateTrigger(t) = s else {
28794            panic!("expected CreateTrigger");
28795        };
28796        assert_eq!(t.name, "tg");
28797        assert_eq!(t.table, "messages");
28798        assert_eq!(t.timing, TriggerTiming::After);
28799        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28800        assert_eq!(t.for_each, TriggerForEach::Row);
28801        assert_eq!(t.function, "update_sv");
28802    }
28803
28804    #[test]
28805    fn create_trigger_before_delete_execute_procedure_alias() {
28806        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28807        let sql =
28808            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28809        let s = parse(sql);
28810        let Statement::CreateTrigger(t) = s else {
28811            panic!("expected CreateTrigger");
28812        };
28813        assert_eq!(t.timing, TriggerTiming::Before);
28814        assert_eq!(t.events, vec![TriggerEvent::Delete]);
28815    }
28816
28817    #[test]
28818    fn drop_trigger_if_exists_round_trips() {
28819        // No parser support for DROP TRIGGER yet — added in v7.12.5
28820        // alongside the broader DROP …{IF EXISTS} cleanup. The
28821        // AST + Display impls are in place so we round-trip via
28822        // construction:
28823        let s = Statement::DropTrigger {
28824            name: "tg".into(),
28825            table: "messages".into(),
28826            if_exists: true,
28827        };
28828        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28829    }
28830
28831    #[test]
28832    fn trigger_ddl_display_roundtrips_through_parser() {
28833        // CREATE TRIGGER + its referenced CREATE FUNCTION must
28834        // Display → parse → same AST (modulo PL/pgSQL body
28835        // formatting which is parser-canonicalised).
28836        for sql in [
28837            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28838            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28839        ] {
28840            let s = parse(sql);
28841            let printed = s.to_string();
28842            let again = parse_statement(&printed)
28843                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28844            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28845        }
28846    }
28847}