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.
362///
363/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
364/// against PG 18.4: a literal with 300 fractional digits round-trips
365/// identically on both engines, so whatever the note described is gone.
366/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
367/// measurement" rather than under "open".
368/// Kept out of the parse_expr recursion frame — see the call site.
369#[inline(never)]
370fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
371    match parse_decimal_literal(&s) {
372        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
373        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
374        // its exact value as a NumericBig.
375        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
376        // v7.39 (read01 numeric.c) — expand the exponent form.
377        None => match expand_scientific_literal(&s) {
378            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
379                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
380                None if plain
381                    .split_once('.')
382                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
383                {
384                    Ok(Literal::NumericBig(plain))
385                }
386                None => s
387                    .parse::<f64>()
388                    .map(Literal::Float)
389                    .map_err(|_| format!("invalid numeric literal {s:?}")),
390            },
391            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
392            SciExpanded::NotScientific => s
393                .parse::<f64>()
394                .map(Literal::Float)
395                .map_err(|_| format!("invalid numeric literal {s:?}")),
396        },
397    }
398}
399
400fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
401    let (int_part, frac_part) = match s.split_once('.') {
402        Some((i, f)) => (i, f),
403        None => (s, ""),
404    };
405    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
406    // places fell out of the numeric path here, which is why
407    // `pg_typeof(1e-256)` answered double precision and a plain
408    // 256-place decimal aborted the query in the big-decimal converter.
409    if frac_part.len() > u16::MAX as usize {
410        return None;
411    }
412    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
413    digits.push_str(int_part);
414    digits.push_str(frac_part);
415    let mantissa: i128 = digits.parse().ok()?;
416    #[allow(clippy::cast_possible_truncation)]
417    Some((mantissa, frac_part.len() as u16))
418}
419
420/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
421/// record-returning JSON functions that take a `AS alias(col type, …)`
422/// column-definition list in FROM position.
423fn is_json_to_record_name(s: &str) -> bool {
424    s.eq_ignore_ascii_case("jsonb_to_recordset")
425        || s.eq_ignore_ascii_case("jsonb_to_record")
426        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
427        // column-definition list desugars identically (the record base
428        // argument only carries the type; a non-NULL base's field
429        // defaults are a recorded delta, RD-6).
430        || s.eq_ignore_ascii_case("json_populate_record")
431        || s.eq_ignore_ascii_case("jsonb_populate_record")
432        || s.eq_ignore_ascii_case("json_populate_recordset")
433        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
434        || s.eq_ignore_ascii_case("json_to_recordset")
435        || s.eq_ignore_ascii_case("json_to_record")
436}
437
438impl Parser {
439    /// Whether what follows an identifier ends an index key, which is how
440    /// an operator class is told from anything else in that position.
441    fn opclass_position_follows(next: Option<&Token>) -> bool {
442        match next {
443            // `ASC` / `DESC` have their own tokens; matching them as
444            // identifiers named "asc" / "desc" — which the first version of
445            // this did — never fires, and `(c text_pattern_ops DESC)` (which
446            // PG18.4 accepts, verified) went on failing to parse.
447            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
448            Some(Token::Ident(w)) => {
449                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
450            }
451            _ => false,
452        }
453    }
454}
455
456fn is_vector_opclass_name(name: &str) -> bool {
457    let lc = name.to_ascii_lowercase();
458    matches!(
459        lc.as_str(),
460        "vector_cosine_ops"
461            | "vector_l2_ops"
462            | "vector_ip_ops"
463            | "halfvec_cosine_ops"
464            | "halfvec_l2_ops"
465            | "halfvec_ip_ops"
466            | "sq8_cosine_ops"
467            | "sq8_l2_ops"
468            | "sq8_ip_ops"
469            // pg_trgm — trigram operator class. SPG's GIN index
470            // already uses tsvector tokens; trigram-style LIKE
471            // pattern matching still routes through a sequential
472            // scan, but the opclass name is accepted so PG schemas
473            // load.
474            | "gin_trgm_ops"
475            | "gist_trgm_ops"
476            // PG built-in btree opclasses occasionally appear in
477            // pg_dump output for column types with multiple
478            // sort orders (text_pattern_ops, varchar_pattern_ops,
479            // bpchar_pattern_ops).
480            | "text_pattern_ops"
481            | "varchar_pattern_ops"
482            | "bpchar_pattern_ops"
483            | "int4_ops"
484            | "int8_ops"
485            | "text_ops"
486    )
487}
488
489#[derive(Debug, Clone, PartialEq, Eq)]
490pub struct ParseError {
491    pub message: String,
492    /// Index into the token stream where parsing tripped. Not a byte offset.
493    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
494    /// field would grow every `Result<_, ParseError>` slot on the deeply
495    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
496    /// 1-based char position is recovered on the cold error path by
497    /// [`syntax_error_position`], which re-tokenizes to map this token index.
498    pub token_pos: usize,
499}
500
501impl fmt::Display for ParseError {
502    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
503    /// with `parse error at token #N: `, which PG has no equivalent of:
504    /// the message bodies are already PG's verbatim (`LIMIT must not be
505    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
506    /// prefix was SPG's internal token index leaking into every one of
507    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
508    /// character position from it for the ErrorResponse `P`.
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        f.write_str(&self.message)
511    }
512}
513
514impl From<LexError> for ParseError {
515    fn from(e: LexError) -> Self {
516        Self {
517            message: format!("lex: {e}"),
518            token_pos: 0,
519        }
520    }
521}
522
523/// v7.9.30 — parse a single expression (no trailing junk). Used by
524/// the engine to re-hydrate stored partial-index / unique-index
525/// predicates from their canonical Display form. The same Pratt
526/// parser the statement path uses; this entry point just skips the
527/// statement dispatch.
528pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
529    let (tokens, offsets) =
530        lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
531    let mut p = Parser::new(tokens);
532    let expr = p
533        .parse_expr(0)
534        .and_then(|e| p.expect_eof().map(|()| e))
535        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
536    Ok(expr)
537}
538
539/// Parse exactly one statement, swallow an optional trailing `;`, and require
540/// the token stream to end there. PG string semantics.
541pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
542    parse_statement_with(input, false)
543}
544
545/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
546/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
547/// The engine threads its session flag through here.
548pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
549    let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
550        .map_err(|e| shape_lex_error(&e, input))?;
551    // The same session flag names the dialect for both the lexer and
552    // the type mapping.
553    let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
554    let stmt = (|| {
555        let stmt = p.parse_one_statement()?;
556        if matches!(p.peek(), Token::Semicolon) {
557            p.advance();
558        }
559        p.expect_eof()?;
560        Ok(stmt)
561    })()
562    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
563    Ok(stmt)
564}
565
566/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
567/// `syntax error at or near "<token>"` and `syntax error at end of input`
568/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
569/// prose — `expected identifier, got Eof`, `unexpected token From in
570/// expression`, `expected end of input, got Ident("with")` — which named
571/// internal token types and, in the Debug forms, leaked the parser's own
572/// enum into a message clients read.
573///
574/// Applied once on the way out, so every construction site is covered and
575/// the token named is the one the error itself points at. Messages whose
576/// bodies are already PG's verbatim (`LIMIT must not be negative`,
577/// `invalid input syntax for type bigint: "abc"`) are left alone — those
578/// are PG's own errors, not its syntax error.
579fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
580    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
581        return e;
582    }
583    let message = match offending_lexeme(input, offsets, e.token_pos) {
584        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
585        None => "syntax error at end of input".into(),
586    };
587    ParseError {
588        message,
589        token_pos: e.token_pos,
590    }
591}
592
593/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
594/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
595/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
596/// comment at or near "/* x"` — the quoted part runs from the opening
597/// delimiter to the end of the input. SPG reported its own internal
598/// shape instead (`unterminated string literal at byte 7`), which named
599/// a byte offset no client can use.
600fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
601    use lexer::LexErrorKind as K;
602    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
603    let message = match &e.kind {
604        K::UnterminatedString => {
605            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
606        }
607        K::UnterminatedQuotedIdent => {
608            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
609        }
610        K::UnterminatedBlockComment => {
611            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
612        }
613        // PG has no "unknown character" error of its own — the character
614        // is skipped and the parser reports the next token. SPG stops at
615        // the character itself and names it, which is the same shape.
616        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
617        // The number-literal kinds already carry PG's `at or near` form.
618        other => alloc::format!(
619            "{}",
620            lexer::LexError {
621                kind: other.clone(),
622                pos: e.pos,
623            }
624        ),
625    };
626    ParseError {
627        message,
628        token_pos: 0,
629    }
630}
631
632/// The offending token exactly as it appears in the input, or `None` at
633/// end of input. PG echoes the source spelling — a lower-case `frm`
634/// reports as `frm`, not as a canonicalised keyword.
635fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
636    let start = *offsets.get(token_pos)?;
637    if start >= input.len() {
638        return None;
639    }
640    let end = offsets
641        .get(token_pos + 1)
642        .copied()
643        .unwrap_or(input.len())
644        .min(input.len());
645    let seg = input.get(start..end)?.trim();
646    if seg.is_empty() {
647        return None;
648    }
649    // A quoted literal / identifier keeps its inner spaces; anything else
650    // ends at the first whitespace (the segment runs to the NEXT token's
651    // start, which may swallow a comment).
652    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
653        Some(seg)
654    } else {
655        seg.split_whitespace().next()
656    }
657}
658
659/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
660/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
661/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
662/// this re-tokenizes `input` on the cold error path to map the failing token
663/// index to its start byte, then to a character offset. `backslash_escapes`
664/// must match the parse that produced `token_pos` (it barely shifts offsets,
665/// but stay consistent). Returns `None` when the index has no offset or the
666/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
667#[must_use]
668pub fn syntax_error_position(
669    input: &str,
670    backslash_escapes: bool,
671    token_pos: usize,
672) -> Option<usize> {
673    let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
674    let byte_off = *offsets.get(token_pos)?;
675    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
676        return None;
677    }
678    Some(input[..byte_off].chars().count() + 1)
679}
680
681struct Parser {
682    tokens: Vec<Token>,
683    pos: usize,
684    /// v7.39 (round 274) — the session's dialect, carried by the same
685    /// signal that drives string-literal escaping: `SET sql_mode` (only
686    /// MySQL clients and mysqldump preambles emit it) turns it on,
687    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
688    /// it off. Needed here because the two dialects disagree about what
689    /// `REAL` means — see the type mapping below.
690    mysql_dialect: bool,
691    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
692    /// mutually recursive expr/select parsers. Bounded so a deeply
693    /// nested input returns a parse error instead of overflowing
694    /// the stack (embed hosts die on overflow — it is an abort,
695    /// not a catchable error).
696    nest_depth: usize,
697    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
698    /// `random() < p/100` predicate here; the enclosing SELECT
699    /// drains the list after its WHERE parses and ANDs the
700    /// predicates in. parse_bare_select save/restores around its
701    /// FROM+WHERE so nested selects only drain their own.
702    pending_sample_preds: Vec<Expr>,
703    /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
704    /// of `parse_bare_select` (which returns a `SelectStatement` and has
705    /// nowhere to put it) to the caller that lowers the pair to the CTAS
706    /// node. `bool` is `TEMP`.
707    pending_select_into: Option<(String, bool)>,
708    /// v7.39 (round 691) — collation lowering channel, the same shape as
709    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
710    /// information, and `ast::OrderBy` is where this parser keeps ordering
711    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
712    /// variant — puts a new arm on `eval_expr`, which this repo has
713    /// measured to overflow the debug stack. So while an ORDER BY KEY is
714    /// being parsed the postfix loop drops the name here instead of
715    /// refusing it, and the key's parser takes it.
716    ///
717    /// Only inside an ORDER BY key: everywhere else an unperformable
718    /// collation still errors, because accepting one at a COMPARISON and
719    /// ignoring it is the defect F36 exists to close.
720    in_order_by_key: bool,
721    order_key_collation: Option<String>,
722    /// POSITION(sub IN str) — while parsing the needle, the IN
723    /// keyword is the argument separator, not a membership test.
724    /// The postfix loop leaves IN unconsumed when this is set.
725    suppress_in_tail: bool,
726    /// Index of the token the last `advance()` returned — see
727    /// [`Parser::consumed_pos`].
728    last_consumed: usize,
729    /// v7.39 (round 506) — the statement's own text and the byte each token
730    /// starts at, so a MySQL projection item can report the SOURCE TEXT
731    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
732    /// spacing and all. Only filled for a MySQL session — a PG one names
733    /// columns from the parsed shape and pays nothing for this.
734    src: Option<(String, Vec<usize>)>,
735}
736
737/// Max expr/select parser nesting (parens, subqueries, CASE, …).
738/// Real SQL nests a few dozen levels at the extreme. Each nesting level
739/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
740/// exists to turn a deep statement into a catchable parse ERROR: a stack
741/// overflow is an abort, and in the server it does not fail one query, it
742/// takes the process down and every other connection with it.
743///
744/// v7.39 (round 507) — measured, because the figure here used to be a
745/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
746/// in BOTH debug and release"), and the debug half of that is wrong by
747/// more than an order of magnitude:
748///
749///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
750///     this budget and errors. Verified against a live server for nested
751///     derived tables, parens, calls, CASE, IN-subqueries, scalar
752///     subqueries, NOT and unary minus — the server stayed up through all
753///     of them. This is the contract that matters, and it holds.
754///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
755///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
756///     and executing aborts around 8 inside a test thread. The budget is
757///     simply unreachable there, which is why a deep-nesting test has to
758///     ask for a large stack of its own — see `nesting_budget_errors_at`
759///     in the parser tests.
760/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
761/// one place.
762///
763/// There were two copies of this fact: a curated list, used for BARE
764/// names, and — in `try_peek_meta_qualified` — no list at all, which
765/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
766/// the engine to complain about a view it could not materialise. So
767/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
768/// had rows, `pg_catalog.pg_stat_activity` was an error.
769///
770/// PG puts `pg_catalog` at the implicit front of every search_path, so
771/// the two spellings name the same relation and must resolve the same
772/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
773/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
774/// meta_view_result path under their own names and must not be
775/// rewritten; a name that is neither reaches the ordinary resolver,
776/// which reports that the relation does not exist — PG's answer.
777const SYNTHESISED_PG_CATALOGS: &[&str] = &[
778    "pg_am",
779    "pg_attrdef",
780    "pg_attribute",
781    "pg_cast",
782    "pg_db_role_setting",
783    "pg_conversion",
784    "pg_default_acl",
785    "pg_shadow",
786    "pg_sequences",
787    "pg_range",
788    "pg_partitioned_table",
789    "pg_language",
790    "pg_group",
791    "pg_authid",
792    "pg_class",
793    "pg_collation",
794    "pg_constraint",
795    "pg_database",
796    "pg_depend",
797    "pg_amop",
798    "pg_amproc",
799    "pg_opclass",
800    "pg_opfamily",
801    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
802    "pg_description",
803    "pg_enum",
804    "pg_extension",
805    // v7.39 (round 541) — pg_dump reads it for every relation of kind
806    // 'f'. SPG has no foreign tables, so it is empty, which is also
807    // what PG reports on a database that has none.
808    "pg_foreign_table",
809    // v7.39 (round 541) — the empty-by-truth family; see
810    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
811    "pg_event_trigger",
812    "pg_file_settings",
813    "pg_foreign_data_wrapper",
814    "pg_foreign_server",
815    "pg_hba_file_rules",
816    "pg_ident_file_mappings",
817    "pg_init_privs",
818    "pg_parameter_acl",
819    "pg_prepared_xacts",
820    "pg_publication_namespace",
821    "pg_publication_rel",
822    "pg_publication_tables",
823    "pg_replication_origin",
824    "pg_replication_origin_status",
825    "pg_seclabel",
826    "pg_seclabels",
827    "pg_shdepend",
828    "pg_shdescription",
829    "pg_shmem_allocations",
830    "pg_shmem_allocations_numa",
831    "pg_shseclabel",
832    "pg_statistic_ext_data",
833    "pg_stats_ext",
834    "pg_stats_ext_exprs",
835    "pg_subscription_rel",
836    "pg_transform",
837    "pg_user_mapping",
838    "pg_user_mappings",
839    "pg_index",
840    "pg_indexes",
841    "pg_inherits",
842    // v7.39 (round 650) — the text-search catalogs SPG can fill
843    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
844    // token types to dictionaries and SPG has no token-type model,
845    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
846    "pg_ts_config",
847    "pg_ts_config_map",
848    "pg_ts_dict",
849    "pg_ts_parser",
850    "pg_ts_template",
851    "pg_matviews",
852    "pg_namespace",
853    // v7.39 (round 621)
854    "pg_operator",
855    "pg_policies",
856    "pg_policy",
857    "pg_proc",
858    "pg_publication",
859    "pg_replication_slots",
860    "pg_roles",
861    // v7.39 (round 143) — the rewrite-rule listing view.
862    // v7.39 (round 312) — and the rule catalogue itself, which
863    // `pg_get_ruledef(oid)` resolves against.
864    "pg_rewrite",
865    "pg_rules",
866    "pg_sequence",
867    "pg_settings",
868    "pg_stat_archiver",
869    "pg_stat_bgwriter",
870    "pg_stat_checkpointer",
871    "pg_stat_database",
872    "pg_stat_io",
873    "pg_stat_progress_analyze",
874    "pg_auth_members",
875    "pg_stat_progress_create_index",
876    "pg_stat_progress_vacuum",
877    "pg_stat_replication",
878    "pg_stat_slru",
879    "pg_stat_subscription_stats",
880    "pg_stat_user_functions",
881    "pg_stat_user_indexes",
882    "pg_stat_user_tables",
883    "pg_stat_wal",
884    "pg_prepared_statements",
885    "pg_largeobject",
886    "pg_largeobject_metadata",
887    "pg_statistic",
888    "pg_statistic_ext",
889    // v7.38.18 — the readable view over pg_statistic.
890    "pg_stats",
891    "pg_subscription",
892    "pg_tables",
893    "pg_tablespace",
894    // v7.39 (round 502) — the timezone catalogues. SPG resolved
895    // named zones correctly but could not list them, so a client
896    // populating a timezone picker got "relation does not exist".
897    "pg_timezone_abbrevs",
898    "pg_timezone_names",
899    "pg_trigger",
900    "pg_type",
901    "pg_user",
902    "pg_views",
903];
904
905const MAX_NEST_DEPTH: usize = 64;
906
907/// Stack accounting for the nesting budget, test-only.
908///
909/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
910/// that MOVES: a compiler upgrade grew the parser's debug frames and
911/// silently ate the margin until `nesting_budget_errors_cleanly` went
912/// from erroring cleanly to aborting on a stack overflow. A count
913/// cannot notice that on its own, so the budget is measured here and
914/// held to a ceiling.
915///
916/// The reading has to come from a helper whose OWN frame is the same at
917/// every call: debug slot placement does not follow source order, so a
918/// local's address inside the function under test is not that
919/// function's frame boundary. Two earlier probes were wrong that way —
920/// one read `&self.nest_depth`, which is the `Parser`'s address and
921/// never moves at all.
922#[cfg(test)]
923mod frame_meter {
924    extern crate std;
925    use std::cell::Cell;
926
927    // Per-THREAD, not global. `cargo test` runs tests in parallel and
928    // plenty of them parse nested expressions, so shared statics get
929    // stack addresses from several threads at once and the subtraction
930    // below turns into noise — it read 229,772 bytes per level that way,
931    // while passing when the test was run on its own.
932    std::thread_local! {
933        static AT_LO: Cell<usize> = const { Cell::new(0) };
934        static AT_HI: Cell<usize> = const { Cell::new(0) };
935    }
936
937    pub(super) const SAMPLE_LO: usize = 4;
938    pub(super) const SAMPLE_HI: usize = 24;
939
940    #[inline(never)]
941    pub(super) fn record(depth: usize) {
942        let anchor = 0u8;
943        let at = core::ptr::from_ref(&anchor) as usize;
944        if depth == SAMPLE_LO {
945            AT_LO.with(|c| c.set(at));
946        } else if depth == SAMPLE_HI {
947            AT_HI.with(|c| c.set(at));
948        }
949    }
950
951    /// Bytes of stack one nesting level costs, averaged over the span.
952    pub(super) fn bytes_per_level() -> usize {
953        let lo = AT_LO.with(Cell::get);
954        let hi = AT_HI.with(Cell::get);
955        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
956        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
957        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
958    }
959
960    pub(super) fn reset() {
961        AT_LO.with(|c| c.set(0));
962        AT_HI.with(|c| c.set(0));
963    }
964}
965
966/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
967/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
968#[inline(never)]
969fn build_center_call(e: Expr) -> Expr {
970    Expr::FunctionCall {
971        name: alloc::string::String::from("center"),
972        args: alloc::vec![e],
973    }
974}
975
976/// Max consecutive binary operators at ONE precedence level
977/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
978/// parse time but evaluates and drops recursively — depth beyond
979/// this overflows 2 MiB worker stacks (debug eval frames run
980/// multiple KiB). `IN (…)` lists are flat and unaffected.
981const MAX_BINARY_CHAIN: usize = 256;
982
983/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
984/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
985/// it keeps its dedicated path (`parse_table_level_fk`).
986enum NamedTableConstraintKind {
987    Check,
988    Unique,
989    PrimaryKey,
990    Exclude,
991}
992
993impl Parser {
994    fn new(tokens: Vec<Token>) -> Self {
995        Self::new_with_dialect(tokens, false)
996    }
997
998    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
999        Self {
1000            tokens,
1001            mysql_dialect,
1002            in_order_by_key: false,
1003            order_key_collation: None,
1004            pos: 0,
1005            nest_depth: 0,
1006            pending_sample_preds: Vec::new(),
1007            pending_select_into: None,
1008            suppress_in_tail: false,
1009            last_consumed: 0,
1010            src: None,
1011        }
1012    }
1013
1014    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1015    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1016        if self.mysql_dialect {
1017            self.src = Some((input.to_string(), offsets.to_vec()));
1018        }
1019        self
1020    }
1021
1022    /// The source text spanning tokens `start ..= end`, trimmed.
1023    ///
1024    /// The offsets are token STARTS, so the span runs to the start of the
1025    /// token after `end` and gives back the whitespace between them —
1026    /// trimming is what makes `a + b FROM t` end at `b`.
1027    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1028        let (text, offsets) = self.src.as_ref()?;
1029        let from = *offsets.get(start)?;
1030        let to = *offsets.get(end + 1)?;
1031        text.get(from..to).map(str::trim_end)
1032    }
1033
1034    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1035    /// nesting depth, erroring out cleanly past the budget.
1036    fn enter_nested(&mut self) -> Result<(), ParseError> {
1037        self.nest_depth += 1;
1038        #[cfg(test)]
1039        frame_meter::record(self.nest_depth);
1040        if self.nest_depth > MAX_NEST_DEPTH {
1041            self.nest_depth -= 1;
1042            return Err(self.err(alloc::format!(
1043                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1044            )));
1045        }
1046        Ok(())
1047    }
1048
1049    fn peek(&self) -> &Token {
1050        // tokens always ends with Eof; pos is clamped in advance().
1051        &self.tokens[self.pos]
1052    }
1053
1054    fn advance(&mut self) -> Token {
1055        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1056        self.last_consumed = self.pos;
1057        if self.pos + 1 < self.tokens.len() {
1058            self.pos += 1;
1059        }
1060        t
1061    }
1062
1063    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1064    /// returned. It was computed as `pos - 1`, which is wrong at both
1065    /// ends: `advance()` parks on the final Eof rather than running off
1066    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1067    /// input`), and after backtracking `pos` is no longer one past the
1068    /// token that failed. Recorded by `advance()` itself instead.
1069    const fn consumed_pos(&self) -> usize {
1070        self.last_consumed
1071    }
1072
1073    fn err(&self, message: String) -> ParseError {
1074        ParseError {
1075            message,
1076            token_pos: self.pos,
1077        }
1078    }
1079
1080    fn expect_eof(&self) -> Result<(), ParseError> {
1081        if matches!(self.peek(), Token::Eof) {
1082            Ok(())
1083        } else {
1084            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1085        }
1086    }
1087
1088    /// v7.14.0 — swallow every token up to (but not including) the
1089    /// next semicolon / EOF. Used by the dump-noise dispatcher
1090    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1091    /// etc. without modeling each grammar.
1092    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1093    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1094    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1095    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1096    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1097    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1098        let start = self.pos;
1099        self.advance(); // COMMENT
1100        if !matches!(self.peek(), Token::On) {
1101            self.pos = start;
1102            self.consume_until_statement_boundary();
1103            return Ok(Statement::Empty);
1104        }
1105        self.advance(); // ON
1106        let kind = match self.peek() {
1107            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1108            Token::Table => "table".into(),
1109            _ => {
1110                self.consume_until_statement_boundary();
1111                return Ok(Statement::Empty);
1112            }
1113        };
1114        if !matches!(
1115            kind.as_str(),
1116            "table"
1117                | "column"
1118                | "index"
1119                | "view"
1120                | "sequence"
1121                | "schema"
1122                | "type"
1123                | "database"
1124                | "function"
1125        ) {
1126            self.consume_until_statement_boundary();
1127            return Ok(Statement::Empty);
1128        }
1129        self.advance(); // the kind keyword
1130        // The object name. ⚠️ `expect_ident_like` strips a leading
1131        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1132        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1133        // `c`. Read the dotted parts from raw tokens instead, then let a
1134        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1135        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1136        loop {
1137            match self.advance() {
1138                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1139                other if unreserved_keyword_text(&other).is_some() => {
1140                    parts.push(unreserved_keyword_text(&other).unwrap());
1141                }
1142                other => {
1143                    return Err(ParseError {
1144                        message: alloc::format!("expected identifier, got {other:?}"),
1145                        token_pos: self.consumed_pos(),
1146                    });
1147                }
1148            }
1149            if matches!(self.peek(), Token::Dot) {
1150                self.advance();
1151            } else {
1152                break;
1153            }
1154        }
1155        // COLUMN wants `table.column`; every other kind wants a bare name.
1156        let want = if kind == "column" { 2 } else { 1 };
1157        while parts.len() > want {
1158            parts.remove(0);
1159        }
1160        let name = parts.join(".");
1161        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1162        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1163        // error here — a dump carrying one function comment failed to
1164        // restore. The list is consumed (the comment store keys by name;
1165        // overload-precise comments are the function-predicate follow-up).
1166        if matches!(self.peek(), Token::LParen)
1167            && matches!(
1168                kind.as_str(),
1169                "function" | "procedure" | "aggregate" | "routine"
1170            )
1171        {
1172            let mut depth = 0usize;
1173            loop {
1174                match self.advance() {
1175                    Token::LParen => depth += 1,
1176                    Token::RParen => {
1177                        depth -= 1;
1178                        if depth == 0 {
1179                            break;
1180                        }
1181                    }
1182                    Token::Eof => {
1183                        return Err(self.err(alloc::string::String::from(
1184                            "unterminated argument list in COMMENT ON",
1185                        )));
1186                    }
1187                    _ => {}
1188                }
1189            }
1190        }
1191        // `IS`
1192        if !matches!(self.peek(), Token::Is) {
1193            self.expect_keyword_ident("is")?;
1194        } else {
1195            self.advance();
1196        }
1197        let comment = match self.peek() {
1198            Token::Null => {
1199                self.advance();
1200                None
1201            }
1202            _ => Some(self.expect_string_literal()?),
1203        };
1204        Ok(Statement::CommentOn {
1205            kind,
1206            name,
1207            comment,
1208        })
1209    }
1210
1211    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1212    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1213    /// [CASCADE|RESTRICT]`.
1214    ///
1215    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1216    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1217    /// and the no-ON `GRANT role TO role` membership form — parses into
1218    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1219    /// on them still restores.
1220    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1221        self.advance(); // GRANT / REVOKE
1222        // REVOKE's optional `GRANT OPTION FOR` prefix.
1223        let mut grant_option = false;
1224        if !grant
1225            && self.peek_keyword_ident("grant")
1226            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1227        {
1228            self.advance(); // GRANT
1229            self.advance(); // OPTION
1230            self.expect_keyword_ident("for")?;
1231            grant_option = true;
1232        }
1233        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1234        // words each with an optional COLUMN list.
1235        let mut privileges: Vec<GrantPriv> = Vec::new();
1236        if matches!(self.peek(), Token::All) {
1237            self.advance();
1238            if self.peek_keyword_ident("privileges") {
1239                self.advance();
1240            }
1241            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1242            // column only.
1243            if matches!(self.peek(), Token::LParen) {
1244                let columns = self.parse_grant_column_list()?;
1245                privileges.push(GrantPriv {
1246                    word: "ALL".into(),
1247                    columns,
1248                });
1249            }
1250        } else {
1251            loop {
1252                // SELECT and INSERT lex as reserved tokens, so they never
1253                // reach `expect_ident_like` as plain idents; the rest
1254                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1255                // MAINTAIN) are ordinary identifiers.
1256                let w = match self.peek() {
1257                    Token::Select => {
1258                        self.advance();
1259                        "SELECT".to_string()
1260                    }
1261                    Token::Insert => {
1262                        self.advance();
1263                        "INSERT".to_string()
1264                    }
1265                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1266                    // schema / database, and it lexes as a reserved token.
1267                    Token::Create => {
1268                        self.advance();
1269                        "CREATE".to_string()
1270                    }
1271                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1272                    // alice`) these "privilege words" are ROLE NAMES, and a
1273                    // role name is case-sensitive. `priv_from_word` folds case
1274                    // itself when they really are privileges.
1275                    _ => self.expect_ident_like()?,
1276                };
1277                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1278                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1279                let columns = if matches!(self.peek(), Token::LParen) {
1280                    self.parse_grant_column_list()?
1281                } else {
1282                    Vec::new()
1283                };
1284                privileges.push(GrantPriv { word: w, columns });
1285                if matches!(self.peek(), Token::Comma) {
1286                    self.advance();
1287                } else {
1288                    break;
1289                }
1290            }
1291        }
1292        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1293        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1294        if !matches!(self.peek(), Token::On) {
1295            let roles: Vec<String> = core::mem::take(&mut privileges)
1296                .into_iter()
1297                .map(|p| p.word)
1298                .collect();
1299            let grantees = self.parse_grantee_list(grant)?;
1300            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1301            // no admin-option layer: a member cannot re-grant).
1302            self.consume_until_statement_boundary();
1303            return Ok(finish_grant(
1304                grant,
1305                GrantStatement {
1306                    privileges: Vec::new(),
1307                    object: GrantObject::Roles(roles),
1308                    grantees,
1309                    grant_option,
1310                },
1311            ));
1312        }
1313        self.advance(); // ON
1314        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1315        // the enforced case; anything else parses and no-ops.
1316        let mut class = "TABLE";
1317        match self.peek() {
1318            Token::Table => {
1319                self.advance();
1320            }
1321            Token::All => {
1322                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1323                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1324                // IN SCHEMA` stay no-ops and keep their own object class.
1325                self.advance(); // ALL
1326                let kind = match self.peek() {
1327                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1328                    // TABLES has its own token (SHOW TABLES owns it).
1329                    Token::Tables | Token::Table => "tables".to_string(),
1330                    _ => String::new(),
1331                };
1332                if !kind.is_empty() {
1333                    self.advance();
1334                }
1335                // `IN SCHEMA <name>`
1336                if matches!(self.peek(), Token::In) {
1337                    self.advance();
1338                    if self.peek_keyword_ident("schema") {
1339                        self.advance();
1340                        let _schema = self.expect_ident_like()?;
1341                    }
1342                }
1343                if kind != "tables" {
1344                    self.consume_until_statement_boundary();
1345                    return Ok(finish_grant(
1346                        grant,
1347                        GrantStatement {
1348                            privileges,
1349                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1350                            grantees: Vec::new(),
1351                            grant_option,
1352                        },
1353                    ));
1354                }
1355                let grantees = self.parse_grantee_list(grant)?;
1356                self.consume_until_statement_boundary();
1357                return Ok(finish_grant(
1358                    grant,
1359                    GrantStatement {
1360                        privileges,
1361                        object: GrantObject::AllTablesInSchema,
1362                        grantees,
1363                        grant_option,
1364                    },
1365                ));
1366            }
1367            Token::Ident(w) | Token::QuotedIdent(w) => {
1368                let lc = w.to_ascii_lowercase();
1369                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1370                // real objects with real ACLs now.
1371                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1372                    self.advance();
1373                    let mut names: Vec<String> = Vec::new();
1374                    loop {
1375                        let mut parts: Vec<String> = Vec::new();
1376                        loop {
1377                            parts.push(self.expect_ident_like()?);
1378                            if matches!(self.peek(), Token::Dot) {
1379                                self.advance();
1380                            } else {
1381                                break;
1382                            }
1383                        }
1384                        names.push(parts.pop().expect("at least one part"));
1385                        if matches!(self.peek(), Token::Comma) {
1386                            self.advance();
1387                        } else {
1388                            break;
1389                        }
1390                    }
1391                    let grantees = self.parse_grantee_list(grant)?;
1392                    let mut grant_option = grant_option;
1393                    if grant && self.peek_keyword_ident("with") {
1394                        self.advance();
1395                        self.expect_keyword_ident("grant")?;
1396                        self.expect_keyword_ident("option")?;
1397                        grant_option = true;
1398                    }
1399                    self.consume_until_statement_boundary();
1400                    let object = match lc.as_str() {
1401                        "sequence" => GrantObject::Sequences(names),
1402                        "schema" => GrantObject::Schemas(names),
1403                        _ => GrantObject::Databases(names),
1404                    };
1405                    return Ok(finish_grant(
1406                        grant,
1407                        GrantStatement {
1408                            privileges,
1409                            object,
1410                            grantees,
1411                            grant_option,
1412                        },
1413                    ));
1414                }
1415                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1416                // keys functions by NAME, so the argument list parses and is
1417                // dropped (an overload set shares one ACL — recorded residual).
1418                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1419                    self.advance();
1420                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1421                    loop {
1422                        let mut parts: Vec<String> = Vec::new();
1423                        loop {
1424                            parts.push(self.expect_ident_like()?);
1425                            if matches!(self.peek(), Token::Dot) {
1426                                self.advance();
1427                            } else {
1428                                break;
1429                            }
1430                        }
1431                        let fname = parts.pop().expect("at least one part");
1432                        // v7.39 (read01 round 62) — the signature picks the
1433                        // overload, so it is captured.
1434                        let sig = if matches!(self.peek(), Token::LParen) {
1435                            Some(self.parse_function_signature_types()?)
1436                        } else {
1437                            None
1438                        };
1439                        names.push((fname, sig));
1440                        if matches!(self.peek(), Token::Comma) {
1441                            self.advance();
1442                        } else {
1443                            break;
1444                        }
1445                    }
1446                    let grantees = self.parse_grantee_list(grant)?;
1447                    self.consume_until_statement_boundary();
1448                    return Ok(finish_grant(
1449                        grant,
1450                        GrantStatement {
1451                            privileges,
1452                            object: GrantObject::Functions(names),
1453                            grantees,
1454                            grant_option,
1455                        },
1456                    ));
1457                }
1458                if matches!(
1459                    lc.as_str(),
1460                    "type"
1461                        | "domain"
1462                        | "language"
1463                        | "tablespace"
1464                        | "large"
1465                        | "foreign"
1466                        | "parameter"
1467                ) {
1468                    self.consume_until_statement_boundary();
1469                    return Ok(finish_grant(
1470                        grant,
1471                        GrantStatement {
1472                            privileges,
1473                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1474                            grantees: Vec::new(),
1475                            grant_option,
1476                        },
1477                    ));
1478                }
1479                class = "TABLE";
1480            }
1481            _ => {}
1482        }
1483        let _ = class;
1484        // The table list. Schema-qualified names drop their qualifier (SPG is
1485        // single-schema) — but read the dotted parts from raw tokens, since
1486        // `expect_ident_like` would silently swallow the leading part.
1487        let mut tables: Vec<String> = Vec::new();
1488        loop {
1489            let mut parts: Vec<String> = Vec::new();
1490            loop {
1491                parts.push(self.expect_ident_like()?);
1492                if matches!(self.peek(), Token::Dot) {
1493                    self.advance();
1494                } else {
1495                    break;
1496                }
1497            }
1498            tables.push(parts.pop().expect("at least one part"));
1499            if matches!(self.peek(), Token::Comma) {
1500                self.advance();
1501            } else {
1502                break;
1503            }
1504        }
1505        let grantees = self.parse_grantee_list(grant)?;
1506        if grant && self.peek_keyword_ident("with") {
1507            self.advance();
1508            self.expect_keyword_ident("grant")?;
1509            self.expect_keyword_ident("option")?;
1510            grant_option = true;
1511        }
1512        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1513        // to cascade to (no re-granting), so both are accepted and ignored.
1514        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1515            self.advance();
1516        }
1517        Ok(finish_grant(
1518            grant,
1519            GrantStatement {
1520                privileges,
1521                object: GrantObject::Tables(tables),
1522                grantees,
1523                grant_option,
1524            },
1525        ))
1526    }
1527
1528    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1529    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1530    /// words; the caller normalises them into a signature key.
1531    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1532        self.advance(); // (
1533        let mut types: Vec<String> = Vec::new();
1534        if matches!(self.peek(), Token::RParen) {
1535            self.advance();
1536            return Ok(types);
1537        }
1538        loop {
1539            // Collect the words of one argument up to a comma / close paren.
1540            let mut words: Vec<String> = Vec::new();
1541            loop {
1542                match self.peek() {
1543                    Token::Comma | Token::RParen | Token::Eof => break,
1544                    _ => {}
1545                }
1546                let tok = self.advance();
1547                match tok {
1548                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1549                    other => {
1550                        if let Some(w) = unreserved_keyword_text(&other) {
1551                            words.push(w);
1552                        }
1553                    }
1554                }
1555            }
1556            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1557            // themselves several words (`double precision`, `character
1558            // varying`, `timestamp with time zone`), so "two words means the
1559            // first is a parameter name" reads the type off `f(double
1560            // precision)` as `precision`. v7.39 (round 282): recognise the
1561            // multi-word spellings first — a leading word that STARTS one of
1562            // them is part of the type, not a name.
1563            let joined = words.join(" ");
1564            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1565                joined
1566            } else if words.len() >= 2 {
1567                words[1..].join(" ")
1568            } else {
1569                words.first().cloned().unwrap_or_default()
1570            };
1571            types.push(ty);
1572            if matches!(self.peek(), Token::Comma) {
1573                self.advance();
1574            } else {
1575                break;
1576            }
1577        }
1578        if matches!(self.peek(), Token::RParen) {
1579            self.advance();
1580        }
1581        Ok(types)
1582    }
1583
1584    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1585    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1586        self.advance(); // (
1587        let mut cols = Vec::new();
1588        loop {
1589            cols.push(self.expect_ident_like()?);
1590            if matches!(self.peek(), Token::Comma) {
1591                self.advance();
1592            } else {
1593                break;
1594            }
1595        }
1596        if !matches!(self.peek(), Token::RParen) {
1597            return Err(self.err(alloc::format!(
1598                "expected ')' to close the column list, got {:?}",
1599                self.peek()
1600            )));
1601        }
1602        self.advance(); // )
1603        Ok(cols)
1604    }
1605
1606    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1607    /// PUBLIC.
1608    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1609        if grant {
1610            if matches!(self.peek(), Token::To) {
1611                self.advance();
1612            } else {
1613                self.expect_keyword_ident("to")?;
1614            }
1615        } else if matches!(self.peek(), Token::From) {
1616            self.advance();
1617        } else {
1618            self.expect_keyword_ident("from")?;
1619        }
1620        let mut grantees: Vec<String> = Vec::new();
1621        loop {
1622            // `GROUP name` is the legacy spelling of a plain role name.
1623            if self.peek_keyword_ident("group") {
1624                self.advance();
1625            }
1626            if self.peek_keyword_ident("public") {
1627                self.advance();
1628                grantees.push(String::new()); // PUBLIC
1629            } else {
1630                grantees.push(self.expect_ident_like()?);
1631            }
1632            if matches!(self.peek(), Token::Comma) {
1633                self.advance();
1634            } else {
1635                break;
1636            }
1637        }
1638        Ok(grantees)
1639    }
1640
1641    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1642    /// The body keeps its `$N` placeholders; substitution happens at
1643    /// EXECUTE. The declared types are recorded for
1644    /// `pg_prepared_statements.parameter_types` but are not enforced —
1645    /// PG infers when the list is omitted, and SPG resolves the values
1646    /// at substitution time either way.
1647    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1648        let start = self.pos;
1649        self.advance(); // PREPARE
1650        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1651        // different statement that happens to share the keyword. PG
1652        // ships with `max_prepared_transactions = 0` and reports it
1653        // this way; SPG has no prepared-transaction registry, so the
1654        // same wording is the accurate answer rather than a dodge.
1655        // Round 277 turned this from a silent no-op into a confusing
1656        // "expected AS in PREPARE" parse error.
1657        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1658            self.advance();
1659            let gid = match self.advance() {
1660                Token::String(g) => g,
1661                other => {
1662                    return Err(self.err(alloc::format!(
1663                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1664                    )));
1665                }
1666            };
1667            return Ok(Statement::PrepareTransaction(gid));
1668        }
1669        let name = self.expect_ident_like()?;
1670        let mut param_types = Vec::new();
1671        if matches!(self.peek(), Token::LParen) {
1672            self.advance();
1673            loop {
1674                let mut ty = self.expect_ident_like()?;
1675                // A parameterised type name (`numeric(10,2)`,
1676                // `varchar(20)`) keeps its argument list in the text.
1677                if matches!(self.peek(), Token::LParen) {
1678                    let mut depth = 0usize;
1679                    let mut buf = String::from("(");
1680                    loop {
1681                        match self.advance() {
1682                            Token::LParen => {
1683                                depth += 1;
1684                                if depth > 1 {
1685                                    buf.push('(');
1686                                }
1687                            }
1688                            Token::RParen => {
1689                                depth -= 1;
1690                                buf.push(')');
1691                                if depth == 0 {
1692                                    break;
1693                                }
1694                            }
1695                            Token::Comma => buf.push(','),
1696                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1697                            Token::Eof => break,
1698                            _ => {}
1699                        }
1700                    }
1701                    ty.push_str(&buf);
1702                }
1703                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1704                // position, same family as the parameter list above.
1705                let array_suffix = self.consume_array_suffix();
1706                ty.push_str(&array_suffix);
1707                param_types.push(ty);
1708                match self.peek() {
1709                    Token::Comma => {
1710                        self.advance();
1711                    }
1712                    Token::RParen => {
1713                        self.advance();
1714                        break;
1715                    }
1716                    other => {
1717                        return Err(self.err(alloc::format!(
1718                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1719                        )));
1720                    }
1721                }
1722            }
1723        }
1724        if !matches!(self.peek(), Token::As) {
1725            return Err(self.err(alloc::format!(
1726                "expected AS in PREPARE, got {:?}",
1727                self.peek()
1728            )));
1729        }
1730        self.advance();
1731        let body = self.parse_one_statement()?;
1732        // The Parser holds tokens, not the source text, so the
1733        // statement PG reports in `pg_prepared_statements.statement`
1734        // is rebuilt from the AST rather than sliced from the input.
1735        let _ = start;
1736        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1737        if !param_types.is_empty() {
1738            source.push_str(" (");
1739            source.push_str(&param_types.join(", "));
1740            source.push(')');
1741        }
1742        source.push_str(" AS ");
1743        source.push_str(&alloc::format!("{body}"));
1744        Ok(Statement::Prepare {
1745            name,
1746            param_types,
1747            body: alloc::boxed::Box::new(body),
1748            source,
1749        })
1750    }
1751
1752    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1753    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1754        self.advance(); // EXECUTE
1755        let name = self.expect_ident_like()?;
1756        let mut args = Vec::new();
1757        if matches!(self.peek(), Token::LParen) {
1758            self.advance();
1759            if matches!(self.peek(), Token::RParen) {
1760                self.advance();
1761            } else {
1762                loop {
1763                    args.push(self.parse_expr(0)?);
1764                    match self.advance() {
1765                        Token::Comma => {}
1766                        Token::RParen => break,
1767                        other => {
1768                            return Err(self.err(alloc::format!(
1769                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1770                            )));
1771                        }
1772                    }
1773                }
1774            }
1775        }
1776        Ok(Statement::Execute { name, args })
1777    }
1778
1779    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1780    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1781    /// procedure catalog yet, so this reports PG's not-found error
1782    /// (with its HINT) rather than pretending the call ran.
1783    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1784    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1785    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1786        self.advance(); // DISCARD
1787        let target = match self.advance() {
1788            Token::All => DiscardTarget::All,
1789            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1790                "all" => DiscardTarget::All,
1791                "plans" => DiscardTarget::Plans,
1792                "sequences" => DiscardTarget::Sequences,
1793                "temp" | "temporary" => DiscardTarget::Temp,
1794                other => {
1795                    return Err(self.err(format!(
1796                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1797                    )));
1798                }
1799            },
1800            other => {
1801                return Err(self.err(format!(
1802                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1803                )));
1804            }
1805        };
1806        Ok(Statement::Discard(target))
1807    }
1808
1809    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1810    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1811    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1812    /// aggressively the server interrupts, which SPG does not distinguish.
1813    /// Bare `KILL <id>` means CONNECTION.
1814    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1815        self.advance(); // KILL
1816        let mut query_only = false;
1817        loop {
1818            // CONNECTION is a reserved keyword token (it also opens
1819            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1820            // `Token::Connection` rather than a bare ident.
1821            if matches!(self.peek(), Token::Connection) {
1822                self.advance();
1823                break;
1824            }
1825            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1826                break;
1827            };
1828            match w.to_ascii_lowercase().as_str() {
1829                "hard" | "soft" => {
1830                    self.advance();
1831                }
1832                "query" => {
1833                    self.advance();
1834                    query_only = true;
1835                    break;
1836                }
1837                _ => break,
1838            }
1839        }
1840        let id = self.parse_expr(0)?;
1841        Ok(Statement::Kill {
1842            query_only,
1843            id: Box::new(id),
1844        })
1845    }
1846
1847    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1848        self.advance(); // CALL
1849        let name = self.expect_ident_like()?;
1850        self.consume_until_statement_boundary();
1851        Ok(Statement::Call(name))
1852    }
1853
1854    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1855        self.advance(); // DEALLOCATE
1856        // PG accepts an optional noise `PREPARE` keyword here.
1857        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1858            self.advance();
1859        }
1860        if matches!(self.peek(), Token::All) {
1861            self.advance();
1862            return Ok(Statement::Deallocate(None));
1863        }
1864        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1865            self.advance();
1866            return Ok(Statement::Deallocate(None));
1867        }
1868        let name = self.expect_ident_like()?;
1869        Ok(Statement::Deallocate(Some(name)))
1870    }
1871
1872    fn consume_until_statement_boundary(&mut self) {
1873        loop {
1874            match self.peek() {
1875                Token::Semicolon | Token::Eof => return,
1876                _ => self.advance(),
1877            };
1878        }
1879    }
1880
1881    /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1882    /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1883    /// runs after it and eats the rest.
1884    fn scan_database_name(&mut self) -> Option<String> {
1885        // The caller has only PEEKED at `DATABASE`; step past it, or the
1886        // first identifier found is the keyword itself. It was, and
1887        // `pg_database` listed a database called `database`.
1888        if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1889        {
1890            self.advance();
1891        }
1892        for kw in ["if", "not", "exists"] {
1893            if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1894            {
1895                self.advance();
1896            }
1897        }
1898        match self.peek().clone() {
1899            Token::Ident(w) | Token::QuotedIdent(w) => {
1900                self.advance();
1901                Some(w)
1902            }
1903            _ => None,
1904        }
1905    }
1906
1907    /// v7.38.18 — consume to the statement boundary like
1908    /// `consume_until_statement_boundary`, but pick out the collation a
1909    /// `CREATE DATABASE` asked for on the way.
1910    ///
1911    /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1912    /// `LC_CTYPE` does not, because SPG has no separate ctype and
1913    /// pretending to honour it would be the more misleading answer. An
1914    /// `=` between the keyword and the value is optional, as in PG.
1915    ///
1916    /// The whole statement used to be thrown away. Being single-database
1917    /// makes the NAME a no-op; it does not make the collation one.
1918    fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1919        let mut want_value = false;
1920        let mut found: Option<String> = None;
1921        loop {
1922            let tok = self.peek().clone();
1923            match &tok {
1924                Token::Semicolon | Token::Eof => break,
1925                Token::Ident(w) | Token::QuotedIdent(w)
1926                    if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1927                {
1928                    want_value = true;
1929                }
1930                Token::Eq if want_value => {}
1931                Token::String(v) if want_value => {
1932                    found = Some(v.clone());
1933                    want_value = false;
1934                }
1935                Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1936                    found = Some(v.clone());
1937                    want_value = false;
1938                }
1939                _ => want_value = false,
1940            }
1941            self.advance();
1942        }
1943        found
1944    }
1945
1946    /// v7.22 (round-13 T2) — consume to the statement boundary like
1947    /// `consume_until_statement_boundary`, but pick out the sequence
1948    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1949    /// columns) or the first string literal (`nextval('<seq>')`).
1950    /// Schema qualifiers and `::regclass` casts are stripped.
1951    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1952        let mut seq: Option<String> = None;
1953        let mut after_sequence_kw = false;
1954        let mut after_name_kw = false;
1955        loop {
1956            match self.peek().clone() {
1957                Token::Semicolon | Token::Eof => break,
1958                Token::Ident(s) | Token::QuotedIdent(s) => {
1959                    if after_name_kw && seq.is_none() {
1960                        self.advance();
1961                        let mut name = s;
1962                        // `SEQUENCE NAME public.groups_id_seq` — keep
1963                        // the bare name, drop qualifiers.
1964                        while matches!(self.peek(), Token::Dot) {
1965                            self.advance();
1966                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1967                                name = n;
1968                            }
1969                        }
1970                        seq = Some(name);
1971                        after_name_kw = false;
1972                        continue;
1973                    }
1974                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1975                        after_name_kw = true;
1976                        after_sequence_kw = false;
1977                    } else {
1978                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1979                    }
1980                    self.advance();
1981                }
1982                Token::String(s) => {
1983                    if seq.is_none() {
1984                        // `nextval('public.groups_id_seq'::regclass)`
1985                        let bare = s
1986                            .rsplit_once('.')
1987                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1988                        seq = Some(bare);
1989                    }
1990                    self.advance();
1991                }
1992                _ => {
1993                    after_sequence_kw = false;
1994                    after_name_kw = false;
1995                    self.advance();
1996                }
1997            }
1998        }
1999        seq
2000    }
2001
2002    /// v7.39 (round 621) — is the next token the keyword `BY`?
2003    ///
2004    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2005    /// column, table and alias name — and SPG lexed it into a dedicated
2006    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2007    /// two-letter keywords the lexer knew, this was the only one PG leaves
2008    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2009    ///
2010    /// The token is gone; the three clauses that own the word — GROUP BY,
2011    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2012    /// ask this instead. Adding it to the unreserved-identifier table was not
2013    /// enough on its own: identifier positions that match the token shape
2014    /// directly (an index's column list, a table alias) never consult that
2015    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2016    /// Not lexing it as a keyword closes the whole class rather than the two
2017    /// positions that happened to be noticed.
2018    fn peek_is_by(&self) -> bool {
2019        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2020    }
2021
2022    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2023    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2024    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2025    fn consume_drop_behaviour(&mut self) {
2026        if matches!(
2027            self.peek(),
2028            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2029        ) {
2030            self.advance();
2031        }
2032    }
2033
2034    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2035        let first = match self.advance() {
2036            Token::Ident(s) | Token::QuotedIdent(s) => s,
2037            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2038            // per PG's `pg_get_keywords()` classification. SPG tokenizes
2039            // these as named variants for parsing leverage in the
2040            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2041            // `BEGIN`, etc.), but they MUST still be usable as table /
2042            // column / alias names in DDL+DML. Sentori migrations like
2043            // 0001_init.sql ship `release TEXT NOT NULL` in the events
2044            // table — the `events.release` column carries the release
2045            // identifier string. Pre-T4 this triggered "expected
2046            // identifier, got Release" and blocked every drop-in user
2047            // whose schema had a column / alias with one of these names.
2048            other if unreserved_keyword_text(&other).is_some() => {
2049                unreserved_keyword_text(&other).unwrap()
2050            }
2051            other => {
2052                return Err(ParseError {
2053                    message: format!("expected identifier, got {other:?}"),
2054                    token_pos: self.consumed_pos(),
2055                });
2056            }
2057        };
2058        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2059        // qualify every name with `public.` (and pg_catalog.* for
2060        // functions); SPG is single-schema so we discard the
2061        // prefix and return only the trailing ident. Same shape
2062        // also handles MySQL `db.tbl` cross-database refs (SPG
2063        // ignores the db part).
2064        if matches!(self.peek(), Token::Dot) {
2065            self.advance();
2066            match self.advance() {
2067                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2068                other if unreserved_keyword_text(&other).is_some() => {
2069                    return Ok(unreserved_keyword_text(&other).unwrap());
2070                }
2071                other => {
2072                    return Err(ParseError {
2073                        message: format!("expected identifier after '{first}.', got {other:?}"),
2074                        token_pos: self.consumed_pos(),
2075                    });
2076                }
2077            }
2078        }
2079        Ok(first)
2080    }
2081
2082    #[allow(clippy::too_many_lines)]
2083    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2084        // v7.14.0 — empty / comment-only / semicolon-only input
2085        // (after the lexer strips line + block + MySQL
2086        // conditional comments) lands as Statement::Empty.
2087        // pg_dump and mysqldump emit several wrappers that
2088        // collapse to nothing after stripping (`/*!40101 SET …
2089        // */;`, blank lines between statements); the engine
2090        // returns CommandOk no-op so the dump loads cleanly.
2091        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2092            return Ok(Statement::Empty);
2093        }
2094        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2095        // catalog / metadata DDL that has no behavioural effect
2096        // on SPG's single-schema, single-database, single-user
2097        // model. Consume the whole statement up to the next
2098        // semicolon / EOF and return Empty. This is broader than
2099        // the per-keyword DROP / SET / COMMENT arms but lets the
2100        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2101        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2102        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2103        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2104            let lc = s.to_ascii_lowercase();
2105            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2106            if lc == "comment" {
2107                return self.parse_comment_on();
2108            }
2109            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2110            if lc == "grant" || lc == "revoke" {
2111                return self.parse_grant_or_revoke(lc == "grant");
2112            }
2113            // v7.39 (round 277) — the SQL-level prepared-statement
2114            // surface is REAL now. It used to be accepted and dropped
2115            // on the theory that "real execution still happens via the
2116            // extended-query flow" — true only for a driver that uses
2117            // that flow; a plain SQL PREPARE / EXECUTE returned no
2118            // rows at all.
2119            if lc == "prepare" {
2120                return self.parse_prepare();
2121            }
2122            if lc == "execute" {
2123                return self.parse_execute();
2124            }
2125            if lc == "deallocate" {
2126                return self.parse_deallocate();
2127            }
2128            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2129            // accepted and dropped, so an application's stored-procedure
2130            // invocation reported success and did nothing. SPG has no
2131            // procedure catalog, so every CALL names a procedure that
2132            // does not exist — which is exactly what PG says.
2133            if lc == "call" {
2134                return self.parse_call();
2135            }
2136            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2137            // names one connection and acts on it.
2138            if lc == "kill" {
2139                return self.parse_kill();
2140            }
2141            if lc == "discard" {
2142                return self.parse_discard();
2143            }
2144            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2145            // Still performs nothing; the roles are carried out so a name
2146            // that does not exist is refused, as PG18 refuses it.
2147            if lc == "reassign" {
2148                self.advance();
2149                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2150                    self.advance();
2151                }
2152                if self.peek_is_by() {
2153                    self.advance();
2154                }
2155                // Only the roles BEFORE the TO are the ones that must
2156                // exist — `TO` names the new owner, which PG checks as
2157                // well, so both lists are collected.
2158                let mut names = self.take_comma_separated_names();
2159                if matches!(self.peek(), Token::To) {
2160                    self.advance();
2161                    names.extend(self.take_comma_separated_names());
2162                }
2163                self.consume_until_statement_boundary();
2164                return Ok(Statement::ValidateOnly {
2165                    kind: crate::ast::ValidateOnlyKind::RoleName,
2166                    names,
2167                });
2168            }
2169            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2170            // unconditionally with `no security label providers have been
2171            // loaded`, whatever object it names, because none is loaded.
2172            // SPG has none either; accepting it told the caller a label had
2173            // been applied when nothing anywhere records one.
2174            if lc == "security" {
2175                self.consume_until_statement_boundary();
2176                return Ok(Statement::ValidateOnly {
2177                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2178                    names: Vec::new(),
2179                });
2180            }
2181            if is_dump_noise_statement(&lc) {
2182                self.consume_until_statement_boundary();
2183                return Ok(Statement::Empty);
2184            }
2185        }
2186        match self.peek() {
2187            Token::Select => self.parse_select_stmt(),
2188            // v7.37.17 (17.6 siblings) — a statement opening with a
2189            // parenthesized query group: `(SELECT … UNION …)
2190            // INTERSECT …`. parse_bare_select's group arm consumes
2191            // the parens; the select parser handles the outer chain
2192            // and tail.
2193            Token::LParen
2194                if matches!(
2195                    self.tokens.get(self.pos + 1),
2196                    Some(Token::Select | Token::LParen | Token::Values)
2197                ) =>
2198            {
2199                self.parse_select_stmt()
2200            }
2201            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2202            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2203            // Lowers to the same UNION ALL chain the FROM-position
2204            // form uses, then reuses the shared SELECT tail.
2205            Token::Values => {
2206                self.advance(); // VALUES
2207                let mut head = self.parse_values_rows_body()?;
2208                self.parse_select_tail_into(&mut head)?;
2209                Ok(Statement::Select(head))
2210            }
2211            // SQL-standard `TABLE name` shorthand for
2212            // `SELECT * FROM name` — pg_dump never emits it, but
2213            // psql users and PG docs use it constantly. Set-op
2214            // chains and the ORDER BY/LIMIT tail compose like any
2215            // SELECT head.
2216            Token::Table
2217                if matches!(
2218                    self.tokens.get(self.pos + 1),
2219                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2220                ) =>
2221            {
2222                let mut head = self.parse_table_shorthand()?;
2223                self.parse_setop_chain_into(&mut head)?;
2224                self.parse_select_tail_into(&mut head)?;
2225                Ok(Statement::Select(head))
2226            }
2227            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2228            // body is a dollar-quoted plpgsql block (lexer already
2229            // collapsed `$$…$$` into a single Token::String).
2230            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2231            // real PlPgSqlBlock so the engine can EXECUTE it at
2232            // top level instead of silently swallowing. Pre-
2233            // v7.16.2 the parser threw the body away and the
2234            // engine returned CommandOk for the entire DO; that
2235            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2236            // $$` into a SEV-1 silent no-op (the IF + the rename
2237            // were both invisible — mailrs's migrate-042 didn't
2238            // actually run). Now the body parses + executes;
2239            // EmbeddedSql inside the block runs immediately
2240            // against the engine (not deferred — we're at top
2241            // level, not inside a trigger row-write loop).
2242            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2243                self.advance();
2244                let body_text = match self.advance() {
2245                    Token::String(s) => s,
2246                    other => {
2247                        return Err(self.err(alloc::format!(
2248                            "expected dollar-quoted body after DO, got {other:?}"
2249                        )));
2250                    }
2251                };
2252                // Optional `LANGUAGE <name>` trailer (idents only).
2253                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2254                    self.advance();
2255                    let _ = self.expect_ident_like()?;
2256                }
2257                // Parse the body — same shape CREATE FUNCTION
2258                // uses for trigger function bodies. If the body
2259                // doesn't parse cleanly we surface the error
2260                // (better than silent no-op).
2261                let block = parse_plpgsql_body(&body_text)?;
2262                Ok(Statement::DoBlock(block))
2263            }
2264            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2265            // WITH isn't a reserved token in our lexer — comes through
2266            // as `Token::Ident("with")` (case-insensitive).
2267            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2268                self.advance();
2269                self.parse_with_cte_then_select()
2270            }
2271            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2272            // an identifier — not a reserved keyword.
2273            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2274                self.advance();
2275                let mut analyze = false;
2276                let mut suggest = false;
2277                let mut costs_off = false;
2278                let mut buffers = false;
2279                let mut timing_off = false;
2280                let mut settings = false;
2281                let mut wal = false;
2282                let mut summary_off = false;
2283                let mut format = crate::ast::ExplainFormat::Text;
2284                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2285                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2286                // options are comma-separated. Booleans default to ON
2287                // when the value token is omitted (matches PG).
2288                if matches!(self.peek(), Token::LParen) {
2289                    self.advance();
2290                    loop {
2291                        let opt = match self.peek().clone() {
2292                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2293                            other => {
2294                                return Err(self.err(format!(
2295                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2296                                )));
2297                            }
2298                        };
2299                        self.advance();
2300                        if opt.eq_ignore_ascii_case("suggest") {
2301                            suggest = true;
2302                            // SUGGEST takes no explicit value today.
2303                        } else if opt.eq_ignore_ascii_case("costs") {
2304                            // PG syntax: `COSTS [ON | OFF]`. Default
2305                            // when value omitted is ON, so plain
2306                            // `COSTS` is a no-op. `COSTS OFF` flips.
2307                            // `ON` lexes to `Token::On` (reserved
2308                            // keyword in JOIN ... ON contexts); accept
2309                            // it alongside the bare Ident form so the
2310                            // grammar matches PG verbatim.
2311                            let value = match self.peek().clone() {
2312                                Token::On => {
2313                                    self.advance();
2314                                    true
2315                                }
2316                                Token::Ident(v) | Token::QuotedIdent(v)
2317                                    if v.eq_ignore_ascii_case("off") =>
2318                                {
2319                                    self.advance();
2320                                    false
2321                                }
2322                                Token::Ident(v) | Token::QuotedIdent(v)
2323                                    if v.eq_ignore_ascii_case("true") =>
2324                                {
2325                                    self.advance();
2326                                    true
2327                                }
2328                                _ => true,
2329                            };
2330                            costs_off = !value;
2331                        } else if opt.eq_ignore_ascii_case("analyze")
2332                            || opt.eq_ignore_ascii_case("analyse")
2333                        {
2334                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2335                            // Same default-ON rule as ANALYZE keyword form.
2336                            let value = match self.peek().clone() {
2337                                Token::On => {
2338                                    self.advance();
2339                                    true
2340                                }
2341                                Token::Ident(v) | Token::QuotedIdent(v)
2342                                    if v.eq_ignore_ascii_case("off") =>
2343                                {
2344                                    self.advance();
2345                                    false
2346                                }
2347                                Token::Ident(v) | Token::QuotedIdent(v)
2348                                    if v.eq_ignore_ascii_case("true") =>
2349                                {
2350                                    self.advance();
2351                                    true
2352                                }
2353                                _ => true,
2354                            };
2355                            analyze = value;
2356                        } else if opt.eq_ignore_ascii_case("buffers") {
2357                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2358                            let value = match self.peek().clone() {
2359                                Token::On => {
2360                                    self.advance();
2361                                    true
2362                                }
2363                                Token::Ident(v) | Token::QuotedIdent(v)
2364                                    if v.eq_ignore_ascii_case("off") =>
2365                                {
2366                                    self.advance();
2367                                    false
2368                                }
2369                                Token::Ident(v) | Token::QuotedIdent(v)
2370                                    if v.eq_ignore_ascii_case("true") =>
2371                                {
2372                                    self.advance();
2373                                    true
2374                                }
2375                                _ => true,
2376                            };
2377                            buffers = value;
2378                        } else if opt.eq_ignore_ascii_case("timing") {
2379                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2380                            // the measured wall-clock annotation.
2381                            let value = match self.peek().clone() {
2382                                Token::On => {
2383                                    self.advance();
2384                                    true
2385                                }
2386                                Token::Ident(v) | Token::QuotedIdent(v)
2387                                    if v.eq_ignore_ascii_case("off") =>
2388                                {
2389                                    self.advance();
2390                                    false
2391                                }
2392                                Token::Ident(v) | Token::QuotedIdent(v)
2393                                    if v.eq_ignore_ascii_case("true") =>
2394                                {
2395                                    self.advance();
2396                                    true
2397                                }
2398                                _ => true,
2399                            };
2400                            timing_off = !value;
2401                        } else if opt.eq_ignore_ascii_case("settings") {
2402                            settings = true;
2403                        } else if opt.eq_ignore_ascii_case("wal") {
2404                            wal = true;
2405                        } else if opt.eq_ignore_ascii_case("summary") {
2406                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2407                            // gates the trailing Planning/Execution Time
2408                            // lines now (was accept-and-no-op).
2409                            let value = match self.peek().clone() {
2410                                Token::On => {
2411                                    self.advance();
2412                                    true
2413                                }
2414                                Token::Ident(v) | Token::QuotedIdent(v)
2415                                    if v.eq_ignore_ascii_case("off") =>
2416                                {
2417                                    self.advance();
2418                                    false
2419                                }
2420                                Token::Ident(v) | Token::QuotedIdent(v)
2421                                    if v.eq_ignore_ascii_case("true") =>
2422                                {
2423                                    self.advance();
2424                                    true
2425                                }
2426                                _ => true,
2427                            };
2428                            summary_off = !value;
2429                        } else if opt.eq_ignore_ascii_case("verbose")
2430                            || opt.eq_ignore_ascii_case("format")
2431                        {
2432                            // v7.37.22 — accept-but-no-op the remaining
2433                            // PG options so EXPLAIN-using clients
2434                            // (pgAdmin / DataGrip) don't see syntax
2435                            // errors. FORMAT takes a value (text /
2436                            // json / yaml / xml); skip the next token
2437                            // if it's an ident.
2438                            if opt.eq_ignore_ascii_case("format") {
2439                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2440                                {
2441                                    self.advance();
2442                                    format = match v.to_ascii_lowercase().as_str() {
2443                                        "text" => crate::ast::ExplainFormat::Text,
2444                                        "json" => crate::ast::ExplainFormat::Json,
2445                                        "xml" => crate::ast::ExplainFormat::Xml,
2446                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2447                                        other => {
2448                                            return Err(self.err(format!(
2449                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2450                                                 supports text, json, xml, yaml"
2451                                            )));
2452                                        }
2453                                    };
2454                                }
2455                            } else {
2456                                // VERBOSE / SUMMARY take optional ON/OFF;
2457                                // consume if present.
2458                                if matches!(self.peek(), Token::On) {
2459                                    self.advance();
2460                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2461                                    self.peek().clone()
2462                                    && (v.eq_ignore_ascii_case("off")
2463                                        || v.eq_ignore_ascii_case("true"))
2464                                {
2465                                    self.advance();
2466                                    let _ = v;
2467                                }
2468                            }
2469                        } else {
2470                            return Err(self.err(format!(
2471                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2472                            )));
2473                        }
2474                        if matches!(self.peek(), Token::Comma) {
2475                            self.advance();
2476                            continue;
2477                        }
2478                        break;
2479                    }
2480                    if !matches!(self.peek(), Token::RParen) {
2481                        return Err(self.err(format!(
2482                            "expected ')' after EXPLAIN options, got {:?}",
2483                            self.peek()
2484                        )));
2485                    }
2486                    self.advance();
2487                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2488                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2489                {
2490                    self.advance();
2491                    analyze = true;
2492                }
2493                // v7.39 (round 224) — the body may open with WITH (CTEs);
2494                // route through the same CTE-then-SELECT path the top-level
2495                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2496                // too (PG explains INSERT / UPDATE / DELETE).
2497                let inner = match self.peek().clone() {
2498                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2499                        self.advance();
2500                        self.parse_with_cte_then_select()?
2501                    }
2502                    Token::Insert => self.parse_insert_stmt(false)?,
2503                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2504                        self.advance();
2505                        self.parse_update_after_keyword()?
2506                    }
2507                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2508                        self.advance();
2509                        self.parse_delete_after_keyword()?
2510                    }
2511                    _ => self.parse_select_stmt()?,
2512                };
2513                if !matches!(
2514                    inner,
2515                    Statement::Select(_)
2516                        | Statement::Insert(_)
2517                        | Statement::Update(_)
2518                        | Statement::Delete(_)
2519                ) {
2520                    return Err(self.err(format!(
2521                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2522                    )));
2523                }
2524                Ok(Statement::Explain(crate::ast::ExplainStatement {
2525                    analyze,
2526                    inner: Box::new(inner),
2527                    suggest,
2528                    costs_off,
2529                    buffers,
2530                    timing_off,
2531                    settings,
2532                    wal,
2533                    summary_off,
2534                    format,
2535                }))
2536            }
2537            Token::Create => self.parse_create_stmt(),
2538            Token::Insert => self.parse_insert_stmt(false),
2539            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2540            // spelling; route to the same handler. DESC is the
2541            // reserved ORDER BY token, so it gets its own arm.
2542            Token::Ident(s)
2543                if s.eq_ignore_ascii_case("describe")
2544                    && matches!(
2545                        self.tokens.get(self.pos + 1),
2546                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2547                    ) =>
2548            {
2549                self.advance();
2550                let table = self.expect_ident_like()?;
2551                Ok(Statement::ShowColumns(table))
2552            }
2553            Token::Desc
2554                if matches!(
2555                    self.tokens.get(self.pos + 1),
2556                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2557                ) =>
2558            {
2559                self.advance();
2560                let table = self.expect_ident_like()?;
2561                Ok(Statement::ShowColumns(table))
2562            }
2563            // `COPY table [(cols)] TO STDOUT` — the export half of
2564            // pg_dump's COPY pair (the FROM stdin half rides the
2565            // embed import path). Options need a format design and
2566            // error honestly.
2567            Token::Ident(s)
2568                if s.eq_ignore_ascii_case("copy")
2569                    && matches!(
2570                        self.tokens.get(self.pos + 1),
2571                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2572                    ) =>
2573            {
2574                self.advance(); // COPY
2575                let table = self.expect_ident_like()?;
2576                let columns = if matches!(self.peek(), Token::LParen) {
2577                    self.advance();
2578                    let mut cols = alloc::vec![self.expect_ident_like()?];
2579                    while matches!(self.peek(), Token::Comma) {
2580                        self.advance();
2581                        cols.push(self.expect_ident_like()?);
2582                    }
2583                    if !matches!(self.peek(), Token::RParen) {
2584                        return Err(self.err(format!(
2585                            "expected ')' after COPY column list, got {:?}",
2586                            self.peek()
2587                        )));
2588                    }
2589                    self.advance();
2590                    Some(cols)
2591                } else {
2592                    None
2593                };
2594                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2595                // endpoint. (FROM STDIN still rides the wire/import path —
2596                // its data arrives out of band.)
2597                if matches!(self.peek(), Token::From)
2598                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2599                {
2600                    self.advance(); // FROM
2601                    let Token::String(path) = self.advance() else {
2602                        unreachable!()
2603                    };
2604                    let options = self.parse_copy_to_options()?;
2605                    return Ok(Statement::CopyFromFile {
2606                        table,
2607                        columns,
2608                        path,
2609                        options,
2610                    });
2611                }
2612                if !matches!(self.peek(), Token::To) {
2613                    return Err(self.err(format!(
2614                        "COPY: only TO STDOUT is supported here (FROM stdin \
2615                         rides the import path); got {:?}",
2616                        self.peek()
2617                    )));
2618                }
2619                self.advance();
2620                if matches!(self.peek(), Token::String(_)) {
2621                    let Token::String(path) = self.advance() else { unreachable!() };
2622                    let options = self.parse_copy_to_options()?;
2623                    return Ok(Statement::CopyToFile {
2624                        table,
2625                        columns,
2626                        query: None,
2627                        path,
2628                        options,
2629                    });
2630                }
2631                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2632                    return Err(self.err(format!(
2633                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2634                        self.peek()
2635                    )));
2636                }
2637                self.advance();
2638                let options = self.parse_copy_to_options()?;
2639                Ok(Statement::CopyTo {
2640                    table,
2641                    columns,
2642                    query: None,
2643                    options,
2644                })
2645            }
2646            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2647            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2648            // result set is streamed in COPY format (PG's query form).
2649            Token::Ident(s)
2650                if s.eq_ignore_ascii_case("copy")
2651                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2652            {
2653                self.advance(); // COPY
2654                self.advance(); // (
2655                let query = self.parse_select_stmt()?;
2656                if !matches!(self.peek(), Token::RParen) {
2657                    return Err(self.err(format!(
2658                        "expected ')' after COPY query, got {:?}",
2659                        self.peek()
2660                    )));
2661                }
2662                self.advance(); // )
2663                if !matches!(self.peek(), Token::To) {
2664                    return Err(self.err(format!(
2665                        "COPY (query): only TO STDOUT is supported, got {:?}",
2666                        self.peek()
2667                    )));
2668                }
2669                self.advance();
2670                if matches!(self.peek(), Token::String(_)) {
2671                    let Token::String(path) = self.advance() else { unreachable!() };
2672                    let options = self.parse_copy_to_options()?;
2673                    return Ok(Statement::CopyToFile {
2674                        table: String::new(),
2675                        columns: None,
2676                        query: Some(alloc::boxed::Box::new(query)),
2677                        path,
2678                        options,
2679                    });
2680                }
2681                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2682                    return Err(self.err(format!(
2683                        "COPY (query): TO supports STDOUT only, got {:?}",
2684                        self.peek()
2685                    )));
2686                }
2687                self.advance();
2688                let options = self.parse_copy_to_options()?;
2689                Ok(Statement::CopyTo {
2690                    table: String::new(),
2691                    columns: None,
2692                    query: Some(alloc::boxed::Box::new(query)),
2693                    options,
2694                })
2695            }
2696            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2697            // Shares the INSERT body; the replace flag lowers it
2698            // onto ON CONFLICT DO UPDATE with an empty assignment
2699            // list (engine: replace the whole row).
2700            Token::Ident(s)
2701                if s.eq_ignore_ascii_case("replace")
2702                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2703            {
2704                self.parse_insert_stmt(true)
2705            }
2706            Token::Begin => {
2707                self.advance();
2708                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2709                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2710                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2711                // is consumed first, then the trailing modes — including the
2712                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2713                // WORK/TRANSACTION). The explicit level, when present, rides the
2714                // statement so `exec_begin` applies it for this transaction.
2715                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2716                {
2717                    self.advance();
2718                }
2719                let iso = self.parse_isolation_level_clauses()?;
2720                Ok(Statement::Begin(iso))
2721            }
2722            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2723            // for BEGIN. START is contextual in PG too; pattern-match
2724            // on the ident here. Iso clauses are parse-and-ignored,
2725            // same as BEGIN above.
2726            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2727                self.advance();
2728                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2729                {
2730                    return Err(self.err(alloc::format!(
2731                        "expected TRANSACTION after START, got {:?}",
2732                        self.peek()
2733                    )));
2734                }
2735                self.advance();
2736                let iso = self.parse_isolation_level_clauses()?;
2737                Ok(Statement::Begin(iso))
2738            }
2739            Token::Commit => {
2740                self.advance();
2741                // PG: `COMMIT [WORK | TRANSACTION]`.
2742                if let Token::Ident(w) = self.peek()
2743                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2744                {
2745                    self.advance();
2746                }
2747                Ok(Statement::Commit)
2748            }
2749            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2750            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2751            // every transaction with `END;` and the drop-in aborted on
2752            // it. Only reachable at statement start (CASE … END lives
2753            // inside expressions), so no ambiguity.
2754            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2755                self.advance();
2756                if let Token::Ident(w) = self.peek()
2757                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2758                {
2759                    self.advance();
2760                }
2761                Ok(Statement::Commit)
2762            }
2763            Token::Rollback => {
2764                self.advance();
2765                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2766                // savepoint without ending the transaction. Bare
2767                // `ROLLBACK` drops the whole TX.
2768                if matches!(self.peek(), Token::To) {
2769                    self.advance();
2770                    if matches!(self.peek(), Token::Savepoint) {
2771                        self.advance();
2772                    }
2773                    let name = self.expect_ident_like()?;
2774                    Ok(Statement::RollbackToSavepoint(name))
2775                } else {
2776                    Ok(Statement::Rollback)
2777                }
2778            }
2779            Token::Savepoint => {
2780                self.advance();
2781                let name = self.expect_ident_like()?;
2782                Ok(Statement::Savepoint(name))
2783            }
2784            Token::Release => {
2785                self.advance();
2786                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2787                // is optional in standard SQL.
2788                if matches!(self.peek(), Token::Savepoint) {
2789                    self.advance();
2790                }
2791                let name = self.expect_ident_like()?;
2792                Ok(Statement::ReleaseSavepoint(name))
2793            }
2794            Token::Show => {
2795                self.advance();
2796                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2797                // v6.1.2 promoted TABLES to a reserved keyword (for
2798                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2799                // arrives as `Token::Tables` rather than a bare ident.
2800                // USERS / COLUMNS remain bare idents.
2801                let target = match self.advance() {
2802                    Token::Tables => "tables".to_string(),
2803                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2804                    // keyword token; recognise it as the SHOW CREATE
2805                    // dispatch keyword too.
2806                    Token::Create => "create".to_string(),
2807                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2808                    // keyword too; let SHOW INDEX FROM parse.
2809                    Token::Index => "index".to_string(),
2810                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2811                    // reserved (used in aggregate function calls);
2812                    // recognise it here so the parser dispatches
2813                    // to ShowParameter("all") — the engine returns
2814                    // the curated parameter inventory.
2815                    Token::All => "all".to_string(),
2816                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2817                    // spelling for the size of the diagnostics area.
2818                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2819                    // phrase with `syntax error at or near "("`, and a
2820                    // PG session must keep getting exactly that rather
2821                    // than a message about an unknown parameter.
2822                    // `COUNT` arrives as a bare ident; the `(*)` and the
2823                    // trailing keyword are consumed here so the whole
2824                    // form reaches the engine as one parameter name.
2825                    Token::Ident(ref c)
2826                        if self.mysql_dialect
2827                            && c.eq_ignore_ascii_case("count")
2828                            && matches!(self.peek(), Token::LParen) =>
2829                    {
2830                        self.advance();
2831                        if matches!(self.peek(), Token::Star) {
2832                            self.advance();
2833                        }
2834                        if matches!(self.peek(), Token::RParen) {
2835                            self.advance();
2836                        }
2837                        match self.advance() {
2838                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2839                                return Ok(Statement::ShowParameter(
2840                                    "count(*) warnings".to_string(),
2841                                ));
2842                            }
2843                            other => {
2844                                return Err(self.err(format!(
2845                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2846                                )));
2847                            }
2848                        }
2849                    }
2850                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2851                    other => {
2852                        return Err(self.err(format!(
2853                            "expected SHOW target, got {other:?}"
2854                        )));
2855                    }
2856                };
2857                match target.as_str() {
2858                    "tables" => Ok(Statement::ShowTables),
2859                    "users" => Ok(Statement::ShowUsers),
2860                    // v7.38 轴 4 — `SHOW transaction_isolation`
2861                    // returns the currently-selected isolation level.
2862                    "transaction_isolation" => Ok(Statement::ShowParameter(
2863                        "transaction_isolation".to_string(),
2864                    )),
2865                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2866                    // TABLE <t>` returns a 2-column row: (Table,
2867                    // Create Table). mysqldump emits this for every
2868                    // table at scrape time; without it the dump
2869                    // round-trip stalls.
2870                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2871                    // FROM <t>` (also spelled `SHOW INDEX` and
2872                    // `SHOW KEYS`). admin / mysqldump probes use
2873                    // it to list per-table indexes.
2874                    "indexes" | "index" | "keys" => {
2875                        if !matches!(self.peek(), Token::From) {
2876                            return Err(self.err(format!(
2877                                "expected FROM after SHOW INDEXES, got {:?}",
2878                                self.peek()
2879                            )));
2880                        }
2881                        self.advance();
2882                        let table = self.expect_ident_like()?;
2883                        Ok(Statement::ShowIndexes(table))
2884                    }
2885                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2886                    // `SHOW VARIABLES`. Both return a 2-column row
2887                    // set listing server-side state; clients probe
2888                    // them at connect time.
2889                    "status" => Ok(Statement::ShowStatus),
2890                    "variables" => {
2891                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2892                        if matches!(self.peek(), Token::Like) {
2893                            self.advance();
2894                            let pat = match self.advance() {
2895                                Token::String(p) => p,
2896                                other => {
2897                                    return Err(self.err(format!(
2898                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2899                                    )));
2900                                }
2901                            };
2902                            return Ok(Statement::ShowVariablesLike(pat));
2903                        }
2904                        Ok(Statement::ShowVariables)
2905                    }
2906                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2907                    "processlist" => Ok(Statement::ShowProcesslist),
2908                    "create" => {
2909                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2910                        // TABLE is supported in v7.17.
2911                        let kind = match self.advance() {
2912                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2913                            Token::Table => "table".to_string(),
2914                            other => {
2915                                return Err(self.err(format!(
2916                                    "expected TABLE after SHOW CREATE, got {other:?}"
2917                                )));
2918                            }
2919                        };
2920                        if !kind.eq_ignore_ascii_case("table") {
2921                            return Err(self.err(format!(
2922                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2923                            )));
2924                        }
2925                        let name = self.expect_ident_like()?;
2926                        Ok(Statement::ShowCreateTable(name))
2927                    }
2928                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2929                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2930                    // it to populate the database selector at connect
2931                    // time; without it `mysql -p` errors before the
2932                    // first user query.
2933                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2934                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2935                    // keyword on its own; it lands here as a bare
2936                    // ident. Returning all publications + their
2937                    // scope summary.
2938                    "publications" => Ok(Statement::ShowPublications),
2939                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2940                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2941                    "columns" => {
2942                        if !matches!(self.peek(), Token::From) {
2943                            return Err(self.err(format!(
2944                                "expected FROM after SHOW COLUMNS, got {:?}",
2945                                self.peek()
2946                            )));
2947                        }
2948                        self.advance();
2949                        let table = self.expect_ident_like()?;
2950                        Ok(Statement::ShowColumns(table))
2951                    }
2952                    // v7.38 轴 4 surface — `SHOW <param>` for any
2953                    // remaining session / preset parameter name
2954                    // (server_version, search_path, client_encoding,
2955                    // …). The engine's ShowParameter handler does the
2956                    // dispatch; unrecognised names error there with
2957                    // a pointer to pg_settings, not at parse time —
2958                    // so a driver that issues `SHOW spam_setting`
2959                    // gets a clear runtime error instead of a
2960                    // confusing "unknown SHOW target".
2961                    other => {
2962                        // v7.38 (read01 P3.20) — a custom namespaced GUC
2963                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2964                        // consume the dotted tail so it round-trips with
2965                        // `SET app.foo` / `current_setting('app.foo')`.
2966                        let mut full = other.to_string();
2967                        while matches!(self.peek(), Token::Dot) {
2968                            self.advance();
2969                            let seg = self.expect_ident_like()?;
2970                            full.push('.');
2971                            full.push_str(&seg.to_ascii_lowercase());
2972                        }
2973                        Ok(Statement::ShowParameter(full))
2974                    }
2975                }
2976            }
2977            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2978            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2979            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2980            // arrived as a bare ident; tokenising it dedicatedly
2981            // keeps the dispatch tree small.
2982            Token::Drop => {
2983                self.advance();
2984                match self.peek() {
2985                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2986                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2987                    // around DROP ROLE cleanup. SPG has no role-owner
2988                    // model, so consume to boundary as a no-op.
2989                    Token::Ident(s) | Token::QuotedIdent(s)
2990                        if s.eq_ignore_ascii_case("owned") =>
2991                    {
2992                        // v7.39 (round 696) — still a no-op (SPG has no
2993                        // role-owner model), but the ROLE is carried out so
2994                        // the engine can refuse one that does not exist,
2995                        // which is what PG18 does.
2996                        self.advance();
2997                        if self.peek_is_by() {
2998                            self.advance();
2999                        }
3000                        let names = self.take_comma_separated_names();
3001                        self.consume_until_statement_boundary();
3002                        Ok(Statement::ValidateOnly {
3003                            kind: crate::ast::ValidateOnlyKind::RoleName,
3004                            names,
3005                        })
3006                    }
3007                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3008                    // It drops only a TEMPORARY table, and name resolution
3009                    // already prefers the session's own, so the keyword is
3010                    // consumed and the ordinary DROP TABLE path runs.
3011                    Token::Ident(s) | Token::QuotedIdent(s)
3012                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3013                    {
3014                        self.advance();
3015                        if !matches!(self.peek(), Token::Table) {
3016                            return Err(self.err(alloc::format!(
3017                                "expected TABLE after DROP TEMPORARY, got {:?}",
3018                                self.peek()
3019                            )));
3020                        }
3021                        self.parse_drop_table_after_keyword()
3022                    }
3023                    Token::Publication => {
3024                        self.advance();
3025                        // v7.39 (round 754, F31-B4) — the round-753
3026                        // audit probe tripped over the missing
3027                        // `IF EXISTS` here (syntax error).
3028                        let if_exists = self.consume_if_exists();
3029                        let name = self.expect_ident_or_string()?;
3030                        Ok(Statement::DropPublication { name, if_exists })
3031                    }
3032                    Token::Subscription => {
3033                        self.advance();
3034                        let if_exists = self.consume_if_exists();
3035                        let name = self.expect_ident_or_string()?;
3036                        Ok(Statement::DropSubscription { name, if_exists })
3037                    }
3038                    Token::Ident(s) | Token::QuotedIdent(s)
3039                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3040                    {
3041                        self.advance();
3042                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3043                        // login user IS a role in PG, and SPG's store holds
3044                        // both. `IF EXISTS` is accepted on either spelling.
3045                        let if_exists = self.consume_if_exists();
3046                        let name = self.expect_ident_or_string()?;
3047                        Ok(Statement::DropUser { name, if_exists })
3048                    }
3049                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3050                    // CREATE DATABASE has parsed since v7.14 and this did
3051                    // not, so `DROP DATABASE IF EXISTS x` — what every
3052                    // teardown script and pg_dumpall preamble opens with —
3053                    // came back as a syntax error, which IF EXISTS cannot
3054                    // soften. The name is carried so the engine can answer
3055                    // the way PG does; PG never lets this succeed on a
3056                    // single-database server, since the name is either
3057                    // unknown ("database … does not exist", or a notice
3058                    // under IF EXISTS) or the one you are connected to
3059                    // ("cannot drop the currently open database").
3060                    Token::Ident(s) | Token::QuotedIdent(s)
3061                        if s.eq_ignore_ascii_case("database") =>
3062                    {
3063                        self.advance();
3064                        let if_exists = self.consume_if_exists();
3065                        let name = self.expect_ident_or_string()?;
3066                        self.consume_until_statement_boundary();
3067                        Ok(Statement::DropDatabase { name, if_exists })
3068                    }
3069                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3070                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3071                        self.advance();
3072                        let if_exists = self.consume_if_exists();
3073                        let name = self.expect_ident_like()?;
3074                        // ON <table>
3075                        if !matches!(self.peek(), Token::On) {
3076                            return Err(self.err(alloc::format!(
3077                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3078                                self.peek()
3079                            )));
3080                        }
3081                        self.advance();
3082                        let table = self.expect_ident_like()?;
3083                        Ok(Statement::DropTrigger {
3084                            name,
3085                            table,
3086                            if_exists,
3087                        })
3088                    }
3089                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3090                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3091                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3092                        self.advance();
3093                        let if_exists = self.consume_if_exists();
3094                        let name = self.expect_ident_like()?;
3095                        if !matches!(self.peek(), Token::On) {
3096                            return Err(self.err(alloc::format!(
3097                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3098                                self.peek()
3099                            )));
3100                        }
3101                        self.advance();
3102                        let table = self.expect_ident_like()?;
3103                        // Optional CASCADE / RESTRICT — accepted, no effect.
3104                        self.consume_until_statement_boundary();
3105                        Ok(Statement::DropRule {
3106                            name,
3107                            table,
3108                            if_exists,
3109                        })
3110                    }
3111                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3112                    // v7.12.4 ignores any optional arg-list (signature-
3113                    // based overload disambiguation lands in v7.12.5+).
3114                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3115                        self.advance();
3116                        let if_exists = self.consume_if_exists();
3117                        let name = self.expect_ident_like()?;
3118                        // v7.39 (read01 round 62) — the argument list identifies
3119                        // WHICH overload to drop, so it is captured, not
3120                        // discarded. `DROP FUNCTION f` (no list) is legal when
3121                        // the name is unambiguous; the engine enforces that.
3122                        let args = if matches!(self.peek(), Token::LParen) {
3123                            Some(self.parse_function_signature_types()?)
3124                        } else {
3125                            None
3126                        };
3127                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3128                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3129                        // accepted since v7.14 and this one refused outright.
3130                        // pg_dump writes it, so refusing was a parse error in
3131                        // the middle of a restore. SPG drops the function
3132                        // either way — it tracks no dependents to cascade to —
3133                        // which is the same reading the other two give it.
3134                        self.consume_drop_behaviour();
3135                        Ok(Statement::DropFunction {
3136                            name,
3137                            args,
3138                            if_exists,
3139                        })
3140                    }
3141                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3142                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3143                    // emit DROP TABLE IF EXISTS at the head of every
3144                    // CREATE TABLE block so re-importing a dump
3145                    // overwrites prior state. SPG accepts and removes
3146                    // matching tables; CASCADE/RESTRICT trailers
3147                    // accepted silently.
3148                    Token::Table => self.parse_drop_table_after_keyword(),
3149                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3150                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3151                    // for partial-index renames and pgvector
3152                    // migrations. SPG removes the matching index;
3153                    // IF EXISTS makes the drop idempotent.
3154                    Token::Index => {
3155                        self.advance();
3156                        let if_exists = self.consume_if_exists();
3157                        let name = self.expect_ident_like()?;
3158                        if matches!(
3159                            self.peek(),
3160                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3161                                || s.eq_ignore_ascii_case("restrict")
3162                        ) {
3163                            self.advance();
3164                        }
3165                        Ok(Statement::DropIndex { name, if_exists })
3166                    }
3167                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3168                    // [CASCADE|RESTRICT]. SPG is single-database;
3169                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3170                    // name [, name…] [CASCADE | RESTRICT]. Real
3171                    // unregister (was silent no-op pre-v7.17).
3172                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3173                        self.advance();
3174                        let if_exists = self.consume_if_exists();
3175                        let mut names = vec![self.expect_ident_like()?];
3176                        while matches!(self.peek(), Token::Comma) {
3177                            self.advance();
3178                            names.push(self.expect_ident_like()?);
3179                        }
3180                        if matches!(
3181                            self.peek(),
3182                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3183                                || s.eq_ignore_ascii_case("restrict")
3184                        ) {
3185                            self.advance();
3186                        }
3187                        Ok(Statement::DropSchema { names, if_exists })
3188                    }
3189                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3190                    // name [, name…] [CASCADE|RESTRICT].
3191                    Token::Ident(s) | Token::QuotedIdent(s)
3192                        if s.eq_ignore_ascii_case("type") =>
3193                    {
3194                        self.advance();
3195                        let if_exists = self.consume_if_exists();
3196                        let mut names = vec![self.expect_ident_like()?];
3197                        while matches!(self.peek(), Token::Comma) {
3198                            self.advance();
3199                            names.push(self.expect_ident_like()?);
3200                        }
3201                        if matches!(
3202                            self.peek(),
3203                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3204                                || s.eq_ignore_ascii_case("restrict")
3205                        ) {
3206                            self.advance();
3207                        }
3208                        Ok(Statement::DropType { names, if_exists })
3209                    }
3210                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3211                    // name [, name…] [CASCADE|RESTRICT].
3212                    Token::Ident(s) | Token::QuotedIdent(s)
3213                        if s.eq_ignore_ascii_case("domain") =>
3214                    {
3215                        self.advance();
3216                        let if_exists = self.consume_if_exists();
3217                        let mut names = vec![self.expect_ident_like()?];
3218                        while matches!(self.peek(), Token::Comma) {
3219                            self.advance();
3220                            names.push(self.expect_ident_like()?);
3221                        }
3222                        if matches!(
3223                            self.peek(),
3224                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3225                                || s.eq_ignore_ascii_case("restrict")
3226                        ) {
3227                            self.advance();
3228                        }
3229                        Ok(Statement::DropDomain { names, if_exists })
3230                    }
3231                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3232                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3233                    Token::Ident(s) | Token::QuotedIdent(s)
3234                        if s.eq_ignore_ascii_case("materialized") =>
3235                    {
3236                        self.advance();
3237                        let nxt = self.peek().clone();
3238                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3239                        {
3240                            return Err(self.err(alloc::format!(
3241                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3242                            )));
3243                        }
3244                        self.advance();
3245                        let if_exists = self.consume_if_exists();
3246                        let mut names = vec![self.expect_ident_like()?];
3247                        while matches!(self.peek(), Token::Comma) {
3248                            self.advance();
3249                            names.push(self.expect_ident_like()?);
3250                        }
3251                        if matches!(
3252                            self.peek(),
3253                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3254                                || s.eq_ignore_ascii_case("restrict")
3255                        ) {
3256                            self.advance();
3257                        }
3258                        Ok(Statement::DropMaterializedView { names, if_exists })
3259                    }
3260                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3261                    // name [, name…] [CASCADE|RESTRICT].
3262                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3263                        self.advance();
3264                        let if_exists = self.consume_if_exists();
3265                        let mut names = vec![self.expect_ident_like()?];
3266                        while matches!(self.peek(), Token::Comma) {
3267                            self.advance();
3268                            names.push(self.expect_ident_like()?);
3269                        }
3270                        if matches!(
3271                            self.peek(),
3272                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3273                                || s.eq_ignore_ascii_case("restrict")
3274                        ) {
3275                            self.advance();
3276                        }
3277                        Ok(Statement::DropView { names, if_exists })
3278                    }
3279                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3280                    // [CASCADE|RESTRICT]. Real removal from catalog
3281                    // (was a silent no-op pre-v7.17).
3282                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3283                        self.advance();
3284                        let if_exists = self.consume_if_exists();
3285                        let mut names = vec![self.expect_ident_like()?];
3286                        while matches!(self.peek(), Token::Comma) {
3287                            self.advance();
3288                            names.push(self.expect_ident_like()?);
3289                        }
3290                        if matches!(
3291                            self.peek(),
3292                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3293                                || s.eq_ignore_ascii_case("restrict")
3294                        ) {
3295                            self.advance();
3296                        }
3297                        Ok(Statement::DropSequence { names, if_exists })
3298                    }
3299                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3300                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3301                        self.advance();
3302                        self.parse_drop_policy_after_keyword()
3303                    }
3304                    // v7.37.17 (17.6 siblings) — DROP <target> for
3305                    // targets SPG doesn't natively track. pg_dump
3306                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3307                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3308                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3309                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3310                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3311                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3312                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3313                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3314                    // etc. — accept + Empty-return so pg_dump tails
3315                    // load through. Materialized-view drop dispatches
3316                    // to the existing DropTable path when the token
3317                    // is Materialized-View-shaped (elsewhere in
3318                    // this parser).
3319                    Token::Ident(s) | Token::QuotedIdent(s)
3320                        if s.eq_ignore_ascii_case("text")
3321                            // The DROP dispatch matches on PEEK — `text` is
3322                            // not yet consumed, so SEARCH/CONFIGURATION sit
3323                            // at pos+1/pos+2 (the round-695 trap's mirror).
3324                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3325                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3326                    {
3327                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3328                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3329                        // stay in the noise arm below.
3330                        self.advance(); // TEXT
3331                        self.advance(); // SEARCH
3332                        self.advance(); // CONFIGURATION
3333                        let if_exists = self.consume_if_exists();
3334                        let names = self.take_comma_separated_names();
3335                        self.consume_until_statement_boundary();
3336                        if if_exists {
3337                            return Ok(Statement::Empty);
3338                        }
3339                        Ok(Statement::ValidateOnly {
3340                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3341                            names,
3342                        })
3343                    }
3344                    Token::Ident(s) | Token::QuotedIdent(s)
3345                        if matches!(
3346                            s.to_ascii_lowercase().as_str(),
3347                            "type"
3348                                | "domain"
3349                                | "operator"
3350                                | "cast"
3351                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3352                                // TEMPLATE (CONFIGURATION intercepted above).
3353                                | "text"
3354                                | "materialized"
3355                                | "large"
3356                                | "role"
3357                                | "access"
3358                                | "procedure"
3359                                | "routine"
3360                        ) =>
3361                    {
3362                        self.consume_until_statement_boundary();
3363                        Ok(Statement::Empty)
3364                    }
3365                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3366                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3367                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3368                    // foreign-data warning family (round 706) so a
3369                    // CREATE→DROP sequence in a dump stays consistent.
3370                    Token::Ident(s) | Token::QuotedIdent(s)
3371                        if s.eq_ignore_ascii_case("server")
3372                            || s.eq_ignore_ascii_case("foreign") =>
3373                    {
3374                        self.advance();
3375                        self.consume_until_statement_boundary();
3376                        Ok(Statement::ValidateOnly {
3377                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3378                            names: Vec::new(),
3379                        })
3380                    }
3381                    Token::Ident(s) | Token::QuotedIdent(s)
3382                        if s.eq_ignore_ascii_case("collation")
3383                            || s.eq_ignore_ascii_case("tablespace") =>
3384                    {
3385                        let kind = if s.eq_ignore_ascii_case("collation") {
3386                            crate::ast::ValidateOnlyKind::CollationName
3387                        } else {
3388                            crate::ast::ValidateOnlyKind::TablespaceName
3389                        };
3390                        self.advance();
3391                        let if_exists = self.consume_if_exists();
3392                        let names = self.take_comma_separated_names();
3393                        self.consume_until_statement_boundary();
3394                        if if_exists {
3395                            return Ok(Statement::Empty);
3396                        }
3397                        Ok(Statement::ValidateOnly { kind, names })
3398                    }
3399                    Token::Ident(s) | Token::QuotedIdent(s)
3400                        if s.eq_ignore_ascii_case("event") =>
3401                    {
3402                        self.advance();
3403                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3404                        {
3405                            self.advance();
3406                        }
3407                        let if_exists = self.consume_if_exists();
3408                        let names = self.take_comma_separated_names();
3409                        self.consume_until_statement_boundary();
3410                        if if_exists {
3411                            return Ok(Statement::Empty);
3412                        }
3413                        Ok(Statement::ValidateOnly {
3414                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3415                            names,
3416                        })
3417                    }
3418                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3419                    // leave the noise list; see the ValidateOnly kinds.
3420                    Token::Ident(s) | Token::QuotedIdent(s)
3421                        if s.eq_ignore_ascii_case("conversion")
3422                            || s.eq_ignore_ascii_case("language")
3423                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3424                            // FIRST — the first draft looked for it after.
3425                            || s.eq_ignore_ascii_case("procedural") =>
3426                    {
3427                        let kind = if s.eq_ignore_ascii_case("conversion") {
3428                            crate::ast::ValidateOnlyKind::ConversionName
3429                        } else {
3430                            crate::ast::ValidateOnlyKind::LanguageName
3431                        };
3432                        self.advance();
3433                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3434                        {
3435                            self.advance();
3436                        }
3437                        let if_exists = self.consume_if_exists();
3438                        let names = self.take_comma_separated_names();
3439                        self.consume_until_statement_boundary();
3440                        if if_exists {
3441                            return Ok(Statement::Empty);
3442                        }
3443                        Ok(Statement::ValidateOnly { kind, names })
3444                    }
3445                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3446                    // name(argtypes)[, …]`. Parsed for real so the engine
3447                    // can answer as PG does; see Statement::DropAggregate.
3448                    Token::Ident(s) | Token::QuotedIdent(s)
3449                        if s.eq_ignore_ascii_case("aggregate") =>
3450                    {
3451                        self.advance();
3452                        let if_exists = self.consume_if_exists();
3453                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3454                        loop {
3455                            let name = self.expect_ident_like()?;
3456                            if !matches!(self.peek(), Token::LParen) {
3457                                return Err(self.err(alloc::format!(
3458                                    "expected argument list after DROP AGGREGATE {name}"
3459                                )));
3460                            }
3461                            self.advance();
3462                            let mut args: Vec<String> = Vec::new();
3463                            let mut star = false;
3464                            loop {
3465                                match self.peek().clone() {
3466                                    Token::RParen => {
3467                                        self.advance();
3468                                        break;
3469                                    }
3470                                    Token::Star => {
3471                                        self.advance();
3472                                        star = true;
3473                                    }
3474                                    Token::Comma => {
3475                                        self.advance();
3476                                    }
3477                                    _ => {
3478                                        // A type name may be multi-token
3479                                        // (`double precision`); glue idents
3480                                        // until , or ).
3481                                        let mut t = self.expect_ident_like()?;
3482                                        while let Token::Ident(nx) = self.peek() {
3483                                            let nx = nx.clone();
3484                                            self.advance();
3485                                            t.push(' ');
3486                                            t.push_str(&nx);
3487                                        }
3488                                        args.push(t);
3489                                    }
3490                                }
3491                            }
3492                            items.push((name, if star { None } else { Some(args) }));
3493                            if matches!(self.peek(), Token::Comma) {
3494                                self.advance();
3495                            } else {
3496                                break;
3497                            }
3498                        }
3499                        self.consume_until_statement_boundary();
3500                        Ok(Statement::DropAggregate { if_exists, items })
3501                    }
3502                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3503                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3504                    // installed; `IF EXISTS` is the spelling that says do
3505                    // not, and it keeps the no-op.
3506                    Token::Ident(s) | Token::QuotedIdent(s)
3507                        if s.eq_ignore_ascii_case("extension") =>
3508                    {
3509                        self.advance();
3510                        let if_exists = self.consume_if_exists();
3511                        let names = self.take_comma_separated_names();
3512                        self.consume_until_statement_boundary();
3513                        if if_exists {
3514                            return Ok(Statement::Empty);
3515                        }
3516                        Ok(Statement::ValidateOnly {
3517                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3518                            names,
3519                        })
3520                    }
3521                    Token::Ident(s) | Token::QuotedIdent(s)
3522                        if s.eq_ignore_ascii_case("statistics") =>
3523                    {
3524                        self.parse_drop_statistics_after_drop()
3525                    }
3526                    other => Err(self.err(format!(
3527                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3528                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3529                    ))),
3530                }
3531            }
3532            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3533            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3534            // and accepted before the view name. SPG materialised
3535            // views re-evaluate on read (always-fresh semantics), so
3536            // the CONCURRENTLY-vs-serial distinction has no runtime
3537            // effect — the refresh body does not block readers either
3538            // way. Same accept-and-no-op pattern as DETACH PARTITION
3539            // CONCURRENTLY (16.5).
3540            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3541                self.advance();
3542                let nxt = self.peek().clone();
3543                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3544                {
3545                    return Err(self.err(alloc::format!(
3546                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3547                    )));
3548                }
3549                self.advance();
3550                let nxt2 = self.peek().clone();
3551                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3552                {
3553                    return Err(self.err(alloc::format!(
3554                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3555                    )));
3556                }
3557                self.advance();
3558                // Optional CONCURRENTLY noise word — consumed without
3559                // changing semantics.
3560                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3561                {
3562                    self.advance();
3563                }
3564                let name = self.expect_ident_like()?;
3565                let with_data = self.parse_optional_with_data(true)?;
3566                Ok(Statement::RefreshMaterializedView { name, with_data })
3567            }
3568            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3569                self.advance();
3570                self.parse_update_after_keyword()
3571            }
3572            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3573            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3574            // [CASCADE | RESTRICT]. Clears every row from each named
3575            // table. Parses at the top level; the engine dispatcher
3576            // walks Statement::Truncate.
3577            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3578                self.advance();
3579                // Optional TABLE noise word — PG accepts both the reserved
3580                // token and the bare identifier spelling.
3581                if matches!(self.peek(), Token::Table)
3582                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3583                {
3584                    self.advance();
3585                }
3586                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3587                // not absorbed. The lookahead keeps a table genuinely
3588                // called `only` working: the keyword is a keyword only
3589                // when a name follows it.
3590                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3591                    if s.eq_ignore_ascii_case("only"))
3592                    && matches!(
3593                        self.tokens.get(self.pos + 1),
3594                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3595                    );
3596                if only {
3597                    self.advance();
3598                }
3599                // Table names (comma-separated).
3600                let mut tables = Vec::new();
3601                loop {
3602                    tables.push(self.expect_ident_like()?);
3603                    if matches!(self.peek(), Token::Comma) {
3604                        self.advance();
3605                        continue;
3606                    }
3607                    break;
3608                }
3609                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3610                let mut restart_identity = false;
3611                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3612                {
3613                    self.advance();
3614                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3615                    {
3616                        self.advance();
3617                        restart_identity = true;
3618                    }
3619                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3620                {
3621                    self.advance();
3622                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3623                    {
3624                        self.advance();
3625                    }
3626                }
3627                // Optional CASCADE / RESTRICT.
3628                let mut cascade = false;
3629                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3630                {
3631                    self.advance();
3632                    cascade = true;
3633                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3634                {
3635                    self.advance();
3636                }
3637                Ok(Statement::Truncate {
3638                    tables,
3639                    restart_identity,
3640                    cascade,
3641                    only,
3642                })
3643            }
3644            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3645            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3646            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3647            // rows change so the index tree is always up-to-date;
3648            // REINDEX is a strict no-op. Accept the whole statement
3649            // shape to boundary for pg_dump round-trip compatibility.
3650            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3651                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3652                // index bloat to rebuild, so the work stays a no-op, but PG
3653                // validates what it was pointed at and this swallowed the
3654                // name at parse time — `REINDEX TABLE typo` reported
3655                // success. Measured on PG18: INDEX / TABLE name a relation,
3656                // SCHEMA a schema, SYSTEM nothing.
3657                self.advance();
3658                self.parse_reindex_tail()
3659            }
3660            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3661            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3662            // SPG has no MVCC bloat today (Phase D visibility map
3663            // queues with v7.38); the freezer collapses hot-tier
3664            // rows into cold segments automatically. VACUUM is a
3665            // no-op — pg_dump maintenance scripts and Discourse's
3666            // periodic-maintenance path both emit it.
3667            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3668            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3669            // actual bloat, so the pre-MVCC accept-and-ignore posture
3670            // became a silent no-op on a customer's manual reclaim.
3671            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3672            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3673            // ANALYZE is captured, the optional table name is captured.
3674            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3675                self.advance();
3676                // Parenthesised option list: absorb it.
3677                if matches!(self.peek(), Token::LParen) {
3678                    let mut depth = 0usize;
3679                    loop {
3680                        match self.advance() {
3681                            Token::LParen => depth += 1,
3682                            Token::RParen => {
3683                                depth -= 1;
3684                                if depth == 0 {
3685                                    break;
3686                                }
3687                            }
3688                            Token::Eof => break,
3689                            _ => {}
3690                        }
3691                    }
3692                }
3693                let mut analyze = false;
3694                let mut table: Option<String> = None;
3695                loop {
3696                    match self.peek() {
3697                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3698                        // an identifier, so the loop below broke out on it and
3699                        // dropped the table name: `VACUUM FULL nosuch` was
3700                        // accepted where `VACUUM nosuch` was refused.
3701                        Token::Full => {
3702                            self.advance();
3703                        }
3704                        Token::Ident(w) | Token::QuotedIdent(w) => {
3705                            let wl = w.to_ascii_lowercase();
3706                            match wl.as_str() {
3707                                "full" | "freeze" | "verbose" => {
3708                                    self.advance();
3709                                }
3710                                "analyze" | "analyse" => {
3711                                    analyze = true;
3712                                    self.advance();
3713                                }
3714                                _ => {
3715                                    table = Some(self.expect_ident_like()?);
3716                                    break;
3717                                }
3718                            }
3719                        }
3720                        _ => break,
3721                    }
3722                }
3723                // Optional trailing column list / anything else to the
3724                // statement boundary (PG accepts per-column ANALYZE).
3725                self.consume_until_statement_boundary();
3726                Ok(Statement::Vacuum { table, analyze })
3727            }
3728            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3729            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3730            // <index>. PG stores rows in physical order matching
3731            // an index; SPG's hot-tier is append-only + cold-tier
3732            // is segment-frozen, so clustering has no persistent
3733            // effect. Accept-and-no-op for pg_dump compat.
3734            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3735                // v7.39 (round 535) — same as REINDEX above: the relation is
3736                // carried so the engine can refuse one that does not exist.
3737                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3738                self.advance();
3739                self.parse_cluster_tail()
3740            }
3741            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3742            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3743            // optional string payload; UNLISTEN takes a channel or `*`.
3744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3745                self.advance();
3746                let ch = match self.advance() {
3747                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3748                    other => {
3749                        return Err(self.err(format!(
3750                            "expected channel name after LISTEN, got {other:?}"
3751                        )));
3752                    }
3753                };
3754                Ok(Statement::Listen(ch))
3755            }
3756            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3757                self.advance();
3758                let channel = match self.advance() {
3759                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3760                    other => {
3761                        return Err(self.err(format!(
3762                            "expected channel name after NOTIFY, got {other:?}"
3763                        )));
3764                    }
3765                };
3766                let payload = if matches!(self.peek(), Token::Comma) {
3767                    self.advance();
3768                    match self.advance() {
3769                        Token::String(p) => Some(p),
3770                        other => {
3771                            return Err(self.err(format!(
3772                                "expected string payload after NOTIFY <channel>, got {other:?}"
3773                            )));
3774                        }
3775                    }
3776                } else {
3777                    None
3778                };
3779                Ok(Statement::Notify { channel, payload })
3780            }
3781            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3782                self.advance();
3783                match self.advance() {
3784                    Token::Star => Ok(Statement::Unlisten(None)),
3785                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3786                    other => Err(self.err(format!(
3787                        "expected channel name or * after UNLISTEN, got {other:?}"
3788                    ))),
3789                }
3790            }
3791            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3792            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3793            // process-wide write lock today; explicit LOCK has no
3794            // effect. Accept-and-no-op for pg_dump / migration
3795            // compat.
3796            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3797                self.advance();
3798                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3799                // engine holds a process-wide write lock), but the TABLE
3800                // NAME is now carried out so the engine can refuse one that
3801                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3802                // READ|WRITE` is a different statement with the same first
3803                // word; it keeps the old no-op, because a MySQL dump's
3804                // bracket names tables it is about to create.
3805                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3806                    if k.eq_ignore_ascii_case("tables"));
3807                if mysql_tables {
3808                    self.consume_until_statement_boundary();
3809                    return Ok(Statement::Empty);
3810                }
3811                if matches!(self.peek(), Token::Table) {
3812                    self.advance();
3813                }
3814                let names = self.take_comma_separated_names();
3815                self.consume_until_statement_boundary();
3816                Ok(Statement::ValidateOnly {
3817                    kind: crate::ast::ValidateOnlyKind::LockTable,
3818                    names,
3819                })
3820            }
3821            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3822            // durability marker + snapshot in PG. SPG has WAL
3823            // checkpointing on a byte / time schedule (v7.37.10
3824            // 60s / 4 MiB defaults). The bare statement parses to
3825            // `Statement::Empty` here (the no_std engine owns no
3826            // WAL / snapshot); v7.37 Epic Du wires the HOST
3827            // (embedded `Database::execute_buffered`, via
3828            // `sql_is_checkpoint`) to force an immediate synchronous
3829            // checkpoint through `Database::checkpoint` — a real
3830            // durability barrier, matching PG.
3831            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3832                self.advance();
3833                self.consume_until_statement_boundary();
3834                Ok(Statement::Empty)
3835            }
3836            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3837                self.advance();
3838                self.parse_delete_after_keyword()
3839            }
3840            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3841            // ALTER is not a reserved keyword in the lexer — handled
3842            // as a bare ident here.
3843            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3844                self.advance();
3845                self.parse_alter_after_keyword()
3846            }
3847            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3848            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3849            // additions needed.
3850            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3851                self.advance();
3852                self.parse_wait_after_keyword()
3853            }
3854            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3855            // Bare ANALYZE → analyse every user table; ANALYZE
3856            // <name> → re-stats one. The argument is an optional
3857            // ident (or quoted ident); anything else is a parse
3858            // error.
3859            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3860            // `WHERE` filter (carved out per V6_7_DESIGN.md
3861            // STABILITY). Lex order: identifier "compact" → "cold"
3862            // → "segments". Anything else after `COMPACT` is a
3863            // parse error.
3864            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3865                self.advance();
3866                let next = self.peek().clone();
3867                let cold = match next {
3868                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3869                    _ => {
3870                        return Err(
3871                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3872                        );
3873                    }
3874                };
3875                if !cold.eq_ignore_ascii_case("cold") {
3876                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3877                }
3878                self.advance();
3879                let next = self.peek().clone();
3880                let segments = match next {
3881                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3882                    _ => {
3883                        return Err(self.err(format!(
3884                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3885                            self.peek()
3886                        )));
3887                    }
3888                };
3889                if !segments.eq_ignore_ascii_case("segments") {
3890                    return Err(self.err(format!(
3891                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3892                    )));
3893                }
3894                self.advance();
3895                Ok(Statement::CompactColdSegments)
3896            }
3897            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3898            // Parsed as a case-insensitive identifier since MERGE
3899            // isn't a reserved lexer keyword (collides with the
3900            // mysqldump `ALGORITHM = MERGE` view clause if it
3901            // were); the inner parser drives the rest of the
3902            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3903            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3904                self.advance();
3905                self.parse_merge_after_keyword()
3906            }
3907            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3908                self.advance();
3909                let target = match self.peek() {
3910                    Token::Eof | Token::Semicolon => None,
3911                    Token::Ident(_) | Token::QuotedIdent(_) => {
3912                        Some(self.expect_ident_like()?)
3913                    }
3914                    other => {
3915                        return Err(self.err(format!(
3916                            "expected table name or end of statement after ANALYZE, got {other:?}"
3917                        )));
3918                    }
3919                };
3920                // v7.39 (round 776, F31 J7) — the per-column form
3921                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3922                // here while the VACUUM arm already consumed it; SPG
3923                // analyzes whole tables, so the list parses and is
3924                // accepted like the VACUUM path's.
3925                if target.is_some() && matches!(self.peek(), Token::LParen) {
3926                    self.advance();
3927                    loop {
3928                        let _ = self.expect_ident_like()?;
3929                        match self.peek() {
3930                            Token::Comma => {
3931                                self.advance();
3932                            }
3933                            Token::RParen => {
3934                                self.advance();
3935                                break;
3936                            }
3937                            other => {
3938                                return Err(self.err(format!(
3939                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
3940                                )));
3941                            }
3942                        }
3943                    }
3944                }
3945                Ok(Statement::Analyze(target))
3946            }
3947            // v7.12.1 — `SET <name> [TO|=] <value>`. The
3948            // `default_text_search_config` parameter is consumed
3949            // by the FTS function dispatcher; other parameter
3950            // names are recorded but treated as a no-op so PG
3951            // dump output loads.
3952            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3953                self.advance();
3954                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3955                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3956                // …` which the SessionVar path handles). `LOCAL` is the only
3957                // one that changes semantics — it scopes the change to the
3958                // current transaction — so capture it; SESSION / GLOBAL are
3959                // accepted and treated as the default session scope.
3960                let mut set_local = false;
3961                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3962                    let q = s.to_ascii_lowercase();
3963                    if q == "local" || q == "session" || q == "global" {
3964                        set_local = q == "local";
3965                        self.advance();
3966                    }
3967                }
3968                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
3969                // { DEFAULT | <role> }`. pg_dump's ACL section switches
3970                // to the object owner with it. SPG maps it onto the
3971                // session-role machinery (recorded delta RD-10: PG moves
3972                // session_user too; SPG moves the effective role).
3973                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3974                    if s.eq_ignore_ascii_case("authorization"))
3975                {
3976                    self.advance(); // AUTHORIZATION
3977                    let role = match self.peek().clone() {
3978                        Token::Default => {
3979                            self.advance();
3980                            None
3981                        }
3982                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3983                            self.advance();
3984                            Some(s)
3985                        }
3986                        _ => None,
3987                    };
3988                    return Ok(Statement::SetRole(role));
3989                }
3990                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3991                // <collation>]` — change the connection client
3992                // charset. SPG stores UTF-8 always and orders
3993                // bytewise; accept as a no-op.
3994                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3995                {
3996                    self.advance();
3997                    // v7.39 — this used to parse the clause and throw it
3998                    // away ("SPG stores UTF-8 always and orders
3999                    // bytewise; accept as a no-op"). That sentence
4000                    // stopped being true when collations arrived, and
4001                    // once `collation_connection` began driving literal
4002                    // comparison, dropping the COLLATE clause became a
4003                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4004                    // utf8mb4_general_ci` reported back
4005                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4006                    //
4007                    // The charset name is emitted as `names` and the
4008                    // ENGINE expands it, because which collation a
4009                    // charset defaults to is MySQL semantics and belongs
4010                    // beside the rest of them, not in the parser.
4011                    let mut pairs = alloc::vec::Vec::new();
4012                    if matches!(
4013                        self.peek(),
4014                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4015                    ) {
4016                        let charset = match self.advance() {
4017                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4018                            _ => unreachable!("peeked an ident-or-string"),
4019                        };
4020                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4021                    }
4022                    // Optional `COLLATE <name>` — emitted AFTER `names`
4023                    // so it overrides the charset's default, which is
4024                    // what MySQL does.
4025                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4026                    {
4027                        self.advance();
4028                        if matches!(
4029                            self.peek(),
4030                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4031                        ) {
4032                            let coll = match self.advance() {
4033                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4034                                _ => unreachable!("peeked an ident-or-string"),
4035                            };
4036                            pairs.push((
4037                                String::from("collation_connection"),
4038                                crate::ast::SetValue::Ident(coll),
4039                            ));
4040                        }
4041                    }
4042                    if pairs.is_empty() {
4043                        return Ok(Statement::Empty);
4044                    }
4045                    return Ok(Statement::SetParameterList(pairs));
4046                }
4047                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4048                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4049                // uses this to switch to the object owner before
4050                // recreating tables. SPG has no role system so this
4051                // is a no-op.
4052                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4053                {
4054                    self.advance(); // ROLE
4055                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4056                    // reset to the login identity; a name / string sets the
4057                    // effective role that drives current_user + RLS.
4058                    let role = match self.peek().clone() {
4059                        Token::Default => {
4060                            self.advance();
4061                            None
4062                        }
4063                        Token::Ident(s) | Token::QuotedIdent(s)
4064                            if s.eq_ignore_ascii_case("none") =>
4065                        {
4066                            self.advance();
4067                            None
4068                        }
4069                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4070                            self.advance();
4071                            Some(s)
4072                        }
4073                        _ => None,
4074                    };
4075                    return Ok(Statement::SetRole(role));
4076                }
4077                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4078                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4079                // ISO SQL surface). pg_dump prepends this to fix
4080                // the isolation level for the restore session. SPG
4081                // defaults to READ COMMITTED and doesn't yet honor
4082                // session-set isolation across statements — accept
4083                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4084                // per-tx form is handled elsewhere.
4085                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4086                {
4087                    self.advance(); // CHARACTERISTICS
4088                    if matches!(self.peek(), Token::As) {
4089                        self.advance();
4090                    }
4091                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4092                        self.advance();
4093                    }
4094                    // v7.39 — no longer a no-op. The note above said SPG
4095                    // "doesn't yet honor session-set isolation across
4096                    // statements"; it does now, through
4097                    // `default_transaction_isolation`, and measured on
4098                    // PG 18.6 this statement is exactly a way to set it:
4099                    //
4100                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4101                    //       ISOLATION LEVEL REPEATABLE READ;
4102                    //   current_setting('default_transaction_isolation')
4103                    //       -> repeatable read
4104                    //
4105                    // pg_dump prepends this to fix the level for a
4106                    // restore session, so accepting it and doing nothing
4107                    // meant the restore ran at a level nobody chose.
4108                    //
4109                    // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4110                    // still consumed and dropped. `default_transaction_read_only`
4111                    // exists in the GUC inventory but nothing enforces it,
4112                    // and setting a value no code honours is the very
4113                    // defect this version is about — a session told it
4114                    // holds a guarantee it does not.
4115                    let modes = self.parse_isolation_level_clauses()?;
4116                    self.consume_until_statement_boundary();
4117                    let mut pairs: alloc::vec::Vec<(
4118                        alloc::string::String,
4119                        crate::ast::SetValue,
4120                    )> = alloc::vec::Vec::new();
4121                    if let Some(level) = modes.isolation {
4122                        pairs.push((
4123                            alloc::string::String::from("default_transaction_isolation"),
4124                            crate::ast::SetValue::String(alloc::string::String::from(
4125                                level.as_pg_str(),
4126                            )),
4127                        ));
4128                    }
4129                    if let Some(ro) = modes.read_only {
4130                        pairs.push((
4131                            alloc::string::String::from("default_transaction_read_only"),
4132                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4133                                "on"
4134                            } else {
4135                                "off"
4136                            })),
4137                        ));
4138                    }
4139                    return Ok(if pairs.is_empty() {
4140                        Statement::Empty
4141                    } else {
4142                        Statement::SetParameterList(pairs)
4143                    });
4144                }
4145                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4146                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4147                // pg_dump emits this to control the deferrability of
4148                // FK / UNIQUE constraints across a bulk restore. SPG
4149                // has no deferrable-constraint machinery today; the
4150                // FK checker is strict-immediate. Accept-and-no-op
4151                // for pg_dump round-trip compatibility.
4152                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4153                {
4154                    self.advance(); // CONSTRAINTS
4155                    // v7.39 (round 288) — no longer a no-op: the trailing
4156                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4157                    // v7.39 (round 308, V29) — and the names are kept.
4158                    // They used to be skipped over on the way to the
4159                    // DEFERRED keyword, so a named form silently behaved
4160                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4161                    // every deferrable constraint in the transaction.
4162                    let mut names: alloc::vec::Vec<alloc::string::String> =
4163                        alloc::vec::Vec::new();
4164                    if matches!(self.peek(), Token::All) {
4165                        self.advance();
4166                    } else {
4167                        loop {
4168                            let mut n = self.expect_ident_like()?;
4169                            // A schema-qualified name (`public.fk_a`)
4170                            // identifies the same constraint; PG resolves
4171                            // it by the trailing segment.
4172                            while matches!(self.peek(), Token::Dot) {
4173                                self.advance();
4174                                n = self.expect_ident_like()?;
4175                            }
4176                            names.push(n);
4177                            if matches!(self.peek(), Token::Comma) {
4178                                self.advance();
4179                            } else {
4180                                break;
4181                            }
4182                        }
4183                    }
4184                    let deferred = match self.peek() {
4185                        Token::Ident(s) | Token::QuotedIdent(s)
4186                            if s.eq_ignore_ascii_case("deferred") =>
4187                        {
4188                            true
4189                        }
4190                        Token::Ident(s) | Token::QuotedIdent(s)
4191                            if s.eq_ignore_ascii_case("immediate") =>
4192                        {
4193                            false
4194                        }
4195                        other => {
4196                            return Err(self.err(alloc::format!(
4197                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4198                            )));
4199                        }
4200                    };
4201                    self.advance();
4202                    return Ok(Statement::SetConstraints { names, deferred });
4203                }
4204                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4205                // { DEFAULT | '<role>' | <ident> }` (mailrs
4206                // round-10 A.1). pg_dump preamble emits the
4207                // `DEFAULT` form to reset session authorization.
4208                //
4209                // v7.39 (round 697) — this said "SPG has no role system so
4210                // this is a strict no-op". SPG has had one since round 58;
4211                // the comment outlived it, and with it the reason a name
4212                // that is not a role was accepted here. It still switches
4213                // no authorization — what it does now is refuse a role
4214                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4215                // AUTHORIZATION` (handled by the RESET parser
4216                // elsewhere). Reference:
4217                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4218                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4219                {
4220                    self.advance(); // AUTHORIZATION
4221                    match self.peek().clone() {
4222                        Token::Default => {
4223                            self.advance();
4224                        }
4225                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4226                            self.advance();
4227                            return Ok(Statement::ValidateOnly {
4228                                kind: crate::ast::ValidateOnlyKind::RoleName,
4229                                names: alloc::vec![r],
4230                            });
4231                        }
4232                        other => {
4233                            return Err(self.err(alloc::format!(
4234                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4235                            )));
4236                        }
4237                    }
4238                    return Ok(Statement::Empty);
4239                }
4240                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4241                // ISOLATION LEVEL { READ COMMITTED | READ
4242                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4243                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4244                // PG-standard surface. v7.37.8 accepts the syntax
4245                // and tracks the selected level on
4246                // `Engine::current_isolation_level()`; the actual
4247                // MVCC / SSI semantics implementation lands in
4248                // the 轴 4 isolation framework (separate train).
4249                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4250                // internally; SPG behaves the same (effectively
4251                // READ COMMITTED at every level today).
4252                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4253                {
4254                    self.advance(); // TRANSACTION
4255                    let modes = self.parse_isolation_level_clauses()?;
4256                    return Ok(Statement::SetTransaction { modes });
4257                }
4258                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4259                // alias — same accept-as-no-op as SET NAMES.
4260                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4261                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4262                {
4263                    self.advance(); // CHARACTER
4264                    self.advance(); // SET
4265                    if matches!(
4266                        self.peek(),
4267                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4268                    ) {
4269                        self.advance();
4270                    }
4271                    return Ok(Statement::Empty);
4272                }
4273                // v7.39 (GUC) — PG spells the timezone GUC as two
4274                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4275                // where <value> is a string/ident or the LOCAL /
4276                // DEFAULT keyword (both mean "back to the default").
4277                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4278                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4279                {
4280                    self.advance(); // TIME
4281                    self.advance(); // ZONE
4282                    let value = match self.peek().clone() {
4283                        Token::Ident(s)
4284                            if s.eq_ignore_ascii_case("local")
4285                                || s.eq_ignore_ascii_case("default") =>
4286                        {
4287                            self.advance();
4288                            crate::ast::SetValue::Default
4289                        }
4290                        Token::Default => {
4291                            self.advance();
4292                            crate::ast::SetValue::Default
4293                        }
4294                        _ => self.parse_set_value()?,
4295                    };
4296                    return Ok(Statement::SetParameter {
4297                        name: "timezone".into(),
4298                        value,
4299                        local: set_local,
4300                    });
4301                }
4302                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4303                // MySQL USER-variable assignment: its own per-session
4304                // namespace, an arbitrary expression on the right, and `:=`
4305                // as a second spelling of `=`. It used to fall into the
4306                // session-PARAMETER list below, whose values are literals and
4307                // whose store nothing reads back under a `@` name — so the
4308                // assignment reported success and vanished.
4309                //
4310                // A `@@`-prefixed LHS is a real engine setting and keeps the
4311                // old path.
4312                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4313                    return self.parse_set_user_vars();
4314                }
4315                // v7.14.0 — multi-assignment form
4316                // `SET a = 1, b = 2, …`. Single-assignment is the
4317                // 1-element case. Each LHS may be a regular ident
4318                // or a SessionVar (`@VAR` / `@@VAR`).
4319                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4320                loop {
4321                    let lhs = match self.peek().clone() {
4322                        Token::SessionVar(s) => {
4323                            self.advance();
4324                            s
4325                        }
4326                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4327                        other => {
4328                            return Err(self.err(format!(
4329                                "expected parameter name after SET, got {other:?}"
4330                            )));
4331                        }
4332                    };
4333                    // Accept either `=` or the bare `TO` keyword.
4334                    match self.peek() {
4335                        Token::Eq => {
4336                            self.advance();
4337                        }
4338                        Token::To => {
4339                            self.advance();
4340                        }
4341                        other => {
4342                            return Err(self.err(format!(
4343                                "expected `=` or TO after SET {lhs}, got {other:?}"
4344                            )));
4345                        }
4346                    }
4347                    let mut value = self.parse_set_value()?;
4348                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4349                    // `, name TO` continues a MySQL-style multi-assign,
4350                    // anything else is a PG list VALUE
4351                    // (`SET search_path = myschema, public`) folded into
4352                    // one comma-joined string.
4353                    while matches!(self.peek(), Token::Comma) {
4354                        let is_assign = matches!(
4355                            self.tokens.get(self.pos + 1),
4356                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4357                        ) && matches!(
4358                            self.tokens.get(self.pos + 2),
4359                            Some(Token::Eq | Token::To)
4360                        );
4361                        if is_assign {
4362                            break;
4363                        }
4364                        self.advance(); // comma
4365                        let next = self.parse_set_value()?;
4366                        let joined = alloc::format!(
4367                            "{}, {}",
4368                            set_value_text(&value),
4369                            set_value_text(&next)
4370                        );
4371                        value = crate::ast::SetValue::String(joined);
4372                    }
4373                    pairs.push((lhs, value));
4374                    if matches!(self.peek(), Token::Comma) {
4375                        self.advance();
4376                        continue;
4377                    }
4378                    break;
4379                }
4380                if pairs.len() == 1 {
4381                    let (name, value) = pairs.into_iter().next().unwrap();
4382                    Ok(Statement::SetParameter {
4383                        name,
4384                        value,
4385                        local: set_local,
4386                    })
4387                } else {
4388                    Ok(Statement::SetParameterList(pairs))
4389                }
4390            }
4391            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4392            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4393                self.advance();
4394                match self.peek().clone() {
4395                    Token::All => {
4396                        self.advance();
4397                        Ok(Statement::ResetParameter(None))
4398                    }
4399                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4400                        self.advance();
4401                        Ok(Statement::ResetParameter(None))
4402                    }
4403                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4404                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4405                        self.advance();
4406                        Ok(Statement::SetRole(None))
4407                    }
4408                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4409                    // (pg_dump's return from the owner switch).
4410                    Token::Ident(s) | Token::QuotedIdent(s)
4411                        if s.eq_ignore_ascii_case("session")
4412                            && matches!(
4413                                self.tokens.get(self.pos + 1),
4414                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4415                                    if a.eq_ignore_ascii_case("authorization")
4416                            ) =>
4417                    {
4418                        self.advance(); // SESSION
4419                        self.advance(); // AUTHORIZATION
4420                        Ok(Statement::SetRole(None))
4421                    }
4422                    _ => {
4423                        let name = self.parse_set_param_name()?;
4424                        Ok(Statement::ResetParameter(Some(name)))
4425                    }
4426                }
4427            }
4428            // v7.39 (round 218) — server-side cursors.
4429            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4430            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4431            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4432            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4433                self.advance();
4434                match self.peek().clone() {
4435                    Token::All => {
4436                        self.advance();
4437                        Ok(Statement::CloseCursor { name: None })
4438                    }
4439                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4440                        self.advance();
4441                        Ok(Statement::CloseCursor { name: None })
4442                    }
4443                    Token::Ident(n) | Token::QuotedIdent(n) => {
4444                        self.advance();
4445                        Ok(Statement::CloseCursor { name: Some(n) })
4446                    }
4447                    other => Err(self.err(format!(
4448                        "expected cursor name or ALL after CLOSE, got {other:?}"
4449                    ))),
4450                }
4451            }
4452            other => Err(self.err(format!(
4453                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4454                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4455            ))),
4456        }
4457    }
4458
4459    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4460    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4461    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4462    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4463    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4464        self.advance(); // DECLARE
4465        let name = match self.advance() {
4466            Token::Ident(n) | Token::QuotedIdent(n) => n,
4467            other => {
4468                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4469            }
4470        };
4471        let mut scroll: Option<bool> = None;
4472        loop {
4473            match self.peek() {
4474                Token::Ident(s)
4475                    if s.eq_ignore_ascii_case("binary")
4476                        || s.eq_ignore_ascii_case("insensitive")
4477                        || s.eq_ignore_ascii_case("asensitive") =>
4478                {
4479                    self.advance();
4480                }
4481                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4482                    self.advance();
4483                    scroll = Some(true);
4484                }
4485                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4486                {
4487                    self.advance(); // NO
4488                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4489                        return Err(self.err(format!(
4490                            "expected SCROLL after NO in DECLARE, got {:?}",
4491                            self.peek()
4492                        )));
4493                    }
4494                    self.advance();
4495                    scroll = Some(false);
4496                }
4497                _ => break,
4498            }
4499        }
4500        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4501            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4502        }
4503        self.advance();
4504        let mut hold = false;
4505        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4506            self.advance();
4507            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4508                return Err(self.err(format!(
4509                    "expected HOLD after WITH in DECLARE, got {:?}",
4510                    self.peek()
4511                )));
4512            }
4513            self.advance();
4514            hold = true;
4515        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4516            self.advance();
4517            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4518                return Err(self.err(format!(
4519                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4520                    self.peek()
4521                )));
4522            }
4523            self.advance();
4524        }
4525        if !matches!(self.peek(), Token::For) {
4526            return Err(self.err(format!(
4527                "expected FOR before the cursor query, got {:?}",
4528                self.peek()
4529            )));
4530        }
4531        self.advance();
4532        let query = self.parse_one_statement()?;
4533        Ok(Statement::DeclareCursor {
4534            name,
4535            scroll,
4536            hold,
4537            query: alloc::boxed::Box::new(query),
4538        })
4539    }
4540
4541    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4542    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4543    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4544    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4545        use crate::ast::CursorDirection as D;
4546        self.advance(); // FETCH / MOVE
4547        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4548            let neg = if matches!(this.peek(), Token::Minus) {
4549                this.advance();
4550                true
4551            } else {
4552                false
4553            };
4554            match this.advance() {
4555                Token::Integer(v) => Ok(if neg { -v } else { v }),
4556                other => Err(this.err(format!("expected count, got {other:?}"))),
4557            }
4558        };
4559        let direction = match self.peek().clone() {
4560            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4561                self.advance();
4562                D::Next
4563            }
4564            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4565                self.advance();
4566                D::Prior
4567            }
4568            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4569                self.advance();
4570                D::First
4571            }
4572            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4573                self.advance();
4574                D::Last
4575            }
4576            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4577                self.advance();
4578                D::Absolute(signed_count(self)?)
4579            }
4580            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4581                self.advance();
4582                D::Relative(signed_count(self)?)
4583            }
4584            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4585                self.advance();
4586                match self.peek().clone() {
4587                    Token::All => {
4588                        self.advance();
4589                        D::All
4590                    }
4591                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4592                        self.advance();
4593                        D::All
4594                    }
4595                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4596                    _ => D::Next, // bare FORWARD = FORWARD 1
4597                }
4598            }
4599            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4600                self.advance();
4601                match self.peek().clone() {
4602                    Token::All => {
4603                        self.advance();
4604                        D::BackwardAll
4605                    }
4606                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4607                        self.advance();
4608                        D::BackwardAll
4609                    }
4610                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4611                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4612                }
4613            }
4614            Token::All => {
4615                self.advance();
4616                D::All
4617            }
4618            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4619                self.advance();
4620                D::All
4621            }
4622            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4623            // Bare `FETCH <name>` — direction defaults to NEXT.
4624            _ => D::Next,
4625        };
4626        // Optional FROM / IN.
4627        if matches!(self.peek(), Token::From)
4628            || matches!(self.peek(), Token::In)
4629            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4630        {
4631            self.advance();
4632        }
4633        let name = match self.advance() {
4634            Token::Ident(n) | Token::QuotedIdent(n) => n,
4635            other => {
4636                return Err(self.err(format!("expected cursor name, got {other:?}")));
4637            }
4638        };
4639        Ok(if is_move {
4640            Statement::MoveCursor { name, direction }
4641        } else {
4642            Statement::FetchCursor { name, direction }
4643        })
4644    }
4645
4646    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4647    /// [(kind, …)] ON <col>, … FROM <table>`.
4648    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4649        self.advance(); // STATISTICS
4650        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4651        let mut if_not_exists = false;
4652        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4653            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4654        {
4655            self.advance();
4656            self.advance();
4657            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4658                self.advance();
4659                if_not_exists = true;
4660            }
4661        }
4662        let name = self.expect_ident_like()?;
4663        let mut kinds = Vec::new();
4664        if matches!(self.peek(), Token::LParen) {
4665            self.advance();
4666            loop {
4667                let k = self.expect_ident_like()?;
4668                // PG stores the single letters; accept the spelled-out
4669                // names the SQL uses and record what PG records.
4670                kinds.push(match k.to_ascii_lowercase().as_str() {
4671                    "ndistinct" => String::from("d"),
4672                    "dependencies" => String::from("f"),
4673                    "mcv" => String::from("m"),
4674                    other => {
4675                        return Err(
4676                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4677                        );
4678                    }
4679                });
4680                match self.advance() {
4681                    Token::Comma => {}
4682                    Token::RParen => break,
4683                    other => {
4684                        return Err(self.err(alloc::format!(
4685                            "expected ',' or ')' in statistics kind list, got {other:?}"
4686                        )));
4687                    }
4688                }
4689            }
4690        }
4691        if !matches!(self.peek(), Token::On) {
4692            return Err(self.err(alloc::format!(
4693                "expected ON in CREATE STATISTICS, got {:?}",
4694                self.peek()
4695            )));
4696        }
4697        self.advance();
4698        let mut columns = Vec::new();
4699        loop {
4700            columns.push(self.expect_ident_like()?);
4701            if matches!(self.peek(), Token::Comma) {
4702                self.advance();
4703            } else {
4704                break;
4705            }
4706        }
4707        if !matches!(self.peek(), Token::From) {
4708            return Err(self.err(alloc::format!(
4709                "expected FROM in CREATE STATISTICS, got {:?}",
4710                self.peek()
4711            )));
4712        }
4713        self.advance();
4714        let table = self.expect_ident_like()?;
4715        Ok(Statement::CreateStatistics {
4716            name,
4717            if_not_exists,
4718            kinds,
4719            columns,
4720            table,
4721        })
4722    }
4723
4724    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4725    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4726    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4727    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4728    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4729    /// forward call.
4730    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4731        self.advance(); // TABLE
4732        let if_exists = self.consume_if_exists();
4733        let mut names: Vec<String> = Vec::new();
4734        loop {
4735            names.push(self.expect_ident_like()?);
4736            if matches!(self.peek(), Token::Comma) {
4737                self.advance();
4738                continue;
4739            }
4740            break;
4741        }
4742        if matches!(
4743            self.peek(),
4744            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4745                || s.eq_ignore_ascii_case("restrict")
4746        ) {
4747            self.advance();
4748        }
4749        Ok(Statement::DropTable { names, if_exists })
4750    }
4751
4752    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4753        self.advance(); // STATISTICS
4754        let mut if_exists = false;
4755        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4756            && matches!(self.tokens.get(self.pos + 1),
4757                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4758        {
4759            self.advance();
4760            self.advance();
4761            if_exists = true;
4762        }
4763        let name = self.expect_ident_like()?;
4764        Ok(Statement::DropStatistics { name, if_exists })
4765    }
4766
4767    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4768        debug_assert!(matches!(self.peek(), Token::Create));
4769        self.advance();
4770        match self.peek() {
4771            Token::Table => self.parse_create_table_stmt_after_create(),
4772            Token::Index => self.parse_create_index_stmt_after_create(false),
4773            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4774            // object now. It used to be consumed by the CREATE-noise
4775            // arm, so a pg_dump that declares extended statistics
4776            // restored silently without them and reflection showed
4777            // nothing.
4778            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4779                self.parse_create_statistics_after_create()
4780            }
4781            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4782            // The `UNIQUE` modifier turns a partial index into a
4783            // partial-uniqueness invariant (only rows matching the
4784            // WHERE predicate are checked for duplicates). mailrs
4785            // K1 (3 hits: email_templates default, calendar_events
4786            // master, calendar_events instance).
4787            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4788                self.advance();
4789                if !matches!(self.peek(), Token::Index) {
4790                    return Err(self.err(alloc::format!(
4791                        "expected INDEX after CREATE UNIQUE, got {:?}",
4792                        self.peek()
4793                    )));
4794                }
4795                self.parse_create_index_stmt_after_create(true)
4796            }
4797            Token::Publication => {
4798                self.advance();
4799                self.parse_create_publication_after_keyword()
4800            }
4801            Token::Subscription => {
4802                self.advance();
4803                self.parse_create_subscription_after_keyword()
4804            }
4805            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4806            // USER isn't a reserved keyword — we look for the bare
4807            // identifier so the lexer doesn't have to grow a token.
4808            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4809                self.advance();
4810                self.parse_create_user_after_keyword(true)
4811            }
4812            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4813            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4814            // the default of the LOGIN attribute.
4815            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4816                self.advance();
4817                self.parse_create_user_after_keyword(false)
4818            }
4819            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4820            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4821                self.advance();
4822                self.parse_create_policy_after_keyword()
4823            }
4824            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4825            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4826            // no-op. mailrs follow-up F3.
4827            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4828                self.advance();
4829                self.parse_create_extension_after_keyword()
4830            }
4831            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4832            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4833            // optional; absorb it here and forward to the
4834            // per-kind parsers with the flag. OR is a reserved
4835            // keyword token.
4836            Token::Or => {
4837                self.advance();
4838                let next = self.peek();
4839                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4840                    return Err(self.err(alloc::format!(
4841                        "expected REPLACE after CREATE OR, got {next:?}"
4842                    )));
4843                };
4844                if !s2.eq_ignore_ascii_case("replace") {
4845                    return Err(self.err(alloc::format!(
4846                        "expected REPLACE after CREATE OR, got {s2:?}"
4847                    )));
4848                }
4849                self.advance();
4850                self.parse_create_function_or_trigger_after_or_replace(true)
4851            }
4852            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4853                self.advance();
4854                self.parse_create_function_after_keyword(false)
4855            }
4856            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4857                self.advance();
4858                self.parse_create_trigger_after_keyword(false)
4859            }
4860            // v7.39 (round 139) — CREATE RULE …
4861            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4862                self.advance();
4863                self.parse_create_rule_after_keyword(false)
4864            }
4865            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4866            // trigger is a row-level AFTER trigger that additionally carries
4867            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4868            // path already tolerates and skips those clauses, so consuming the
4869            // CONSTRAINT keyword and reusing it makes the statement parse and the
4870            // trigger fire. (The deferral timing itself is not yet honoured —
4871            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4872            // for every non-deferred use.)
4873            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4874                self.advance();
4875                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4876                    if t.eq_ignore_ascii_case("trigger"))
4877                {
4878                    return Err(self.err(alloc::format!(
4879                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4880                        self.peek()
4881                    )));
4882                }
4883                self.advance();
4884                self.parse_create_trigger_after_keyword(false)
4885            }
4886            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4887            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4888                self.advance();
4889                self.parse_create_sequence_after_keyword(false)
4890            }
4891            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4892            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4893                self.advance();
4894                self.parse_create_view_after_keyword(false, false, false)
4895            }
4896            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4897            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4898            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4899            // appear (in any order) between `CREATE` and `VIEW` in
4900            // every mysqldump-emitted view. Pre-2.6 the parser
4901            // rejected the prefix and the customer's whole view
4902            // backup failed on the first view. The hints are pure
4903            // planner / permission metadata; SPG's view-rewrite
4904            // path is semantically equivalent for all three
4905            // algorithms in v7.17 (TEMPTABLE differs only in
4906            // perf for huge views — out of v7.17 scope), and
4907            // DEFINER / SQL SECURITY are pure single-user
4908            // permissioning that SPG ignores by design.
4909            Token::Ident(s) | Token::QuotedIdent(s)
4910                if s.eq_ignore_ascii_case("algorithm")
4911                    || s.eq_ignore_ascii_case("definer")
4912                    || s.eq_ignore_ascii_case("sql") =>
4913            {
4914                self.consume_mysql_view_prefix()?;
4915                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4916                // (in any order, in any combination), the next
4917                // keyword must be VIEW. mysqldump never emits these
4918                // prefixes on non-view statements.
4919                let next = self.peek().clone();
4920                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4921                    if s2.eq_ignore_ascii_case("view"))
4922                {
4923                    self.advance();
4924                    self.parse_create_view_after_keyword(false, false, false)
4925                } else {
4926                    Err(self.err(alloc::format!(
4927                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4928                    )))
4929                }
4930            }
4931            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4932            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4933                self.advance();
4934                self.parse_create_type_after_keyword()
4935            }
4936            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4937            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4938            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4939                self.advance();
4940                self.parse_create_domain_after_keyword()
4941            }
4942            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4943            // name [AUTHORIZATION user]. Real catalog registry
4944            // (was silent-no-op'd pre-v7.17).
4945            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4946                self.advance();
4947                let if_not_exists = self.parse_if_not_exists();
4948                let name = self.expect_ident_like()?;
4949                // Optional `AUTHORIZATION <user>` trailer — accepted,
4950                // ignored (single-user catalog).
4951                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4952                    if s.eq_ignore_ascii_case("authorization"))
4953                {
4954                    self.advance();
4955                    let _ = self.expect_ident_like()?;
4956                }
4957                Ok(Statement::CreateSchema { name, if_not_exists })
4958            }
4959            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4960            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4961                self.advance();
4962                let next = self.peek().clone();
4963                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4964                {
4965                    self.advance();
4966                    self.parse_create_materialized_view_after_keyword()
4967                } else {
4968                    Err(self.err(alloc::format!(
4969                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4970                    )))
4971                }
4972            }
4973            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4974            // no-op below), an UNLOGGED table is a real, fully-usable table in
4975            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4976            // durability optimisation is a follow-up), so a dump / app that
4977            // declares UNLOGGED tables works instead of failing to parse.
4978            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4979                self.advance(); // UNLOGGED
4980                if matches!(self.peek(), Token::Table) {
4981                    self.parse_create_table_stmt_after_create()
4982                } else {
4983                    Err(self.err(format!(
4984                        "expected TABLE after CREATE UNLOGGED, got {:?}",
4985                        self.peek()
4986                    )))
4987                }
4988            }
4989            Token::Ident(s) | Token::QuotedIdent(s)
4990                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4991            {
4992                self.advance();
4993                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4994                let next = self.peek().clone();
4995                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4996                {
4997                    self.advance();
4998                    self.parse_create_sequence_after_keyword(true)
4999                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5000                {
5001                    self.advance();
5002                    self.parse_create_view_after_keyword(false, false, true)
5003                } else {
5004                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5005                    // consumed and answered OK while creating nothing, so
5006                    // every statement that touched the table afterwards failed
5007                    // with "table not found" — the DDL itself lied. It is a
5008                    // real CREATE TABLE now, marked temporary so the executor
5009                    // puts it in the session's own namespace. An optional
5010                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5011                    // is not legal, but the keyword is consumed by the
5012                    // CREATE TABLE parser itself).
5013                    let stmt = self.parse_create_table_stmt_after_create()?;
5014                    match stmt {
5015                        Statement::CreateTable(mut c) => {
5016                            c.temporary = true;
5017                            Ok(Statement::CreateTable(c))
5018                        }
5019                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5020                        // CTAS node, which needs the same session namespace.
5021                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5022                            m.temporary = true;
5023                            Ok(Statement::CreateMaterializedView(m))
5024                        }
5025                        other => Ok(other),
5026                    }
5027                }
5028            }
5029            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5030            // BEGIN <body> END`. The body may reference `@var`
5031            // session variables, SET statements, internal `;`
5032            // terminators, etc. SPG has no procedure runtime, so
5033            // consume the whole `CREATE PROCEDURE … END` block as
5034            // a no-op so mysqldump scripts that include stored
5035            // routines load through. The matching-END consumer
5036            // tracks BEGIN/END nesting depth to handle nested
5037            // BEGIN blocks correctly.
5038            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5039                self.consume_mysql_routine_body();
5040                Ok(Statement::Empty)
5041            }
5042            // v7.14.0 — pg_dump / mysqldump emit
5043            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5044            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5045            // SPG is single-schema / single-database; these have
5046            // no behavioural effect, so consume + return Empty.
5047            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5048            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5049            // moved up to real parser branches. DATABASE / ROLE /
5050            // POLICY / OPERATOR stay no-op forever
5051            // (single-database, hardcoded roles).
5052            Token::Ident(s) | Token::QuotedIdent(s)
5053                if matches!(
5054                    s.to_ascii_lowercase().as_str(),
5055                    "database"
5056                        | "role"
5057                        | "operator"
5058                        | "cast"
5059                        | "aggregate"
5060                        | "language"
5061                        | "collation"
5062                        | "conversion"
5063                        // v7.17.0 Phase 8 (audit N6) — rarely-
5064                        // emitted pg_dump shapes that should
5065                        // load through without a parser error.
5066                        // SPG has no planner statistics catalog,
5067                        // no event-trigger hooks, no foreign-
5068                        // data-wrapper infrastructure; consume
5069                        // + return Empty.
5070                        | "statistics"
5071                        | "event"
5072                        // v7.37.17 (17.6 siblings) — additional CREATE
5073                        // targets pg_dump / operator install scripts
5074                        // may emit that SPG has no matching machinery
5075                        // for. Consume + Empty-return.
5076                        | "text"
5077                        | "tablespace"
5078                        | "access"
5079                        | "large"
5080                ) =>
5081            {
5082                // DATABASE is the one member of this list PG refuses
5083                // inside a transaction block; the rest (ROLE, CAST,
5084                // TABLESPACE, …) it runs there quite happily, so only
5085                // this one is named. Still a no-op otherwise — SPG is
5086                // single-database.
5087                let is_database = s.eq_ignore_ascii_case("database");
5088                // The name is the first token after DATABASE, past an
5089                // `IF NOT EXISTS`.
5090                let name = if is_database {
5091                    self.scan_database_name()
5092                } else {
5093                    None
5094                };
5095                let collation = if is_database {
5096                    self.scan_database_collation_until_boundary()
5097                } else {
5098                    self.consume_until_statement_boundary();
5099                    None
5100                };
5101                if is_database {
5102                    return Ok(Statement::NoOpPreventedInTransaction {
5103                        what: String::from("CREATE DATABASE"),
5104                        collation,
5105                        name,
5106                    });
5107                }
5108                Ok(Statement::Empty)
5109            }
5110            // v7.39 (round 706) — the foreign-data family leaves the silent
5111            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5112            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5113            // FDW machinery), but the ENGINE now warns, so a restore log
5114            // says what will not function instead of reporting success.
5115            Token::Ident(s) | Token::QuotedIdent(s)
5116                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5117            {
5118                self.consume_until_statement_boundary();
5119                Ok(Statement::ValidateOnly {
5120                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5121                    names: Vec::new(),
5122                })
5123            }
5124            other => Err(self.err(format!(
5125                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5126            ))),
5127        }
5128    }
5129
5130    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5131    /// keyword decides whether we parse a function or trigger
5132    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5133    /// PROCEDURE) — those land in later releases.
5134    fn parse_create_function_or_trigger_after_or_replace(
5135        &mut self,
5136        or_replace: bool,
5137    ) -> Result<Statement, ParseError> {
5138        let tok = self.peek();
5139        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5140            return Err(self.err(alloc::format!(
5141                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5142            )));
5143        };
5144        if s.eq_ignore_ascii_case("function") {
5145            self.advance();
5146            self.parse_create_function_after_keyword(or_replace)
5147        } else if s.eq_ignore_ascii_case("trigger") {
5148            self.advance();
5149            self.parse_create_trigger_after_keyword(or_replace)
5150        } else if s.eq_ignore_ascii_case("rule") {
5151            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5152            self.advance();
5153            self.parse_create_rule_after_keyword(or_replace)
5154        } else if s.eq_ignore_ascii_case("view") {
5155            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5156            self.advance();
5157            self.parse_create_view_after_keyword(or_replace, false, false)
5158        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5159            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5160            self.advance();
5161            let nxt = self.peek().clone();
5162            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5163            {
5164                self.advance();
5165                self.parse_create_view_after_keyword(or_replace, false, true)
5166            } else {
5167                Err(self.err(alloc::format!(
5168                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5169                )))
5170            }
5171        } else {
5172            Err(self.err(alloc::format!(
5173                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5174            )))
5175        }
5176    }
5177
5178    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5179    /// SPG doesn't have a registry; pgvector / similar are
5180    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5181    /// the syntax lets dual-target schemas keep the line.
5182    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5183        // Optional `IF NOT EXISTS`.
5184        self.consume_if_not_exists();
5185        let name = self.expect_ident_like()?;
5186        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5187        // CASCADE / FROM '<v>' clauses; we don't model them.
5188        loop {
5189            match self.peek() {
5190                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5191                    self.advance();
5192                    continue;
5193                }
5194                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5195                    self.advance();
5196                    let _ = self.expect_ident_like()?;
5197                    continue;
5198                }
5199                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5200                    self.advance();
5201                    // String or ident literal.
5202                    let _ = self.advance();
5203                    continue;
5204                }
5205                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5206                    self.advance();
5207                    let _ = self.advance();
5208                    continue;
5209                }
5210                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5211                    self.advance();
5212                    continue;
5213                }
5214                _ => break,
5215            }
5216        }
5217        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5218        // nosuch` reported success and `pg_extension` then did not list it,
5219        // which is the accept-and-do-nothing shape F31 exists to find.
5220        Ok(Statement::ValidateOnly {
5221            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5222            names: alloc::vec![name],
5223        })
5224    }
5225
5226    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5227    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5228    /// already been consumed by the caller. Grammar accepted:
5229    ///
5230    ///   name `(` arg-list `)`
5231    ///   `RETURNS` return-type
5232    ///   [ `LANGUAGE` ident ]
5233    ///   `AS` $$ body $$
5234    ///   [ `LANGUAGE` ident ]
5235    ///
5236    /// Either `LANGUAGE` position is allowed; PG accepts both.
5237    fn parse_create_function_after_keyword(
5238        &mut self,
5239        or_replace: bool,
5240    ) -> Result<Statement, ParseError> {
5241        let name = self.expect_ident_like()?;
5242        // Argument list. v7.12.4 commonly sees the empty `()`
5243        // (trigger functions); typed args parse and round-trip
5244        // but the executor only invokes nullary functions.
5245        if !matches!(self.peek(), Token::LParen) {
5246            return Err(self.err(alloc::format!(
5247                "expected '(' after function name {name:?}, got {:?}",
5248                self.peek()
5249            )));
5250        }
5251        self.advance();
5252        let args = self.parse_function_arg_list()?;
5253        // RETURNS clause.
5254        let tok = self.peek();
5255        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5256            return Err(self.err(alloc::format!(
5257                "expected RETURNS after function arg list, got {tok:?}"
5258            )));
5259        };
5260        if !s.eq_ignore_ascii_case("returns") {
5261            return Err(self.err(alloc::format!(
5262                "expected RETURNS after function arg list, got {s:?}"
5263            )));
5264        }
5265        self.advance();
5266        let returns = self.parse_function_return()?;
5267        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5268        // re-check after the body too).
5269        let mut language: Option<String> = self.parse_optional_language()?;
5270        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5271        // either side of the body and in any order, interleaved with
5272        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5273        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5274        // PG's own pg_dump output did not restore.
5275        let mut attrs = FunctionAttrs::default();
5276        loop {
5277            let before = self.pos;
5278            self.parse_function_attrs_into(&mut attrs)?;
5279            if language.is_none() {
5280                language = self.parse_optional_language()?;
5281            }
5282            if self.pos == before {
5283                break;
5284            }
5285        }
5286        // `AS` followed by a $$-quoted body (lexer already
5287        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5288        // Token::String). AS is a reserved keyword (Token::As).
5289        if !matches!(self.peek(), Token::As) {
5290            return Err(self.err(alloc::format!(
5291                "expected AS before function body, got {:?}",
5292                self.peek()
5293            )));
5294        }
5295        self.advance();
5296        let body_text = match self.peek() {
5297            Token::String(s) => {
5298                let body = s.clone();
5299                self.advance();
5300                body
5301            }
5302            other => {
5303                return Err(self.err(alloc::format!(
5304                    "expected $$-quoted function body after AS, got {other:?}"
5305                )));
5306            }
5307        };
5308        // Trailing clauses — PG's other accepted position for both the
5309        // LANGUAGE and the attributes.
5310        loop {
5311            let before = self.pos;
5312            self.parse_function_attrs_into(&mut attrs)?;
5313            if language.is_none() {
5314                language = self.parse_optional_language()?;
5315            }
5316            if self.pos == before {
5317                break;
5318            }
5319        }
5320        let language = language.unwrap_or_else(|| String::from("sql"));
5321        // PL/pgSQL bodies get structure-parsed. Other languages
5322        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5323        // recognise) round-trip as Raw text — the executor errors
5324        // when invoked with a clear unsupported message.
5325        let body = if language.eq_ignore_ascii_case("plpgsql") {
5326            match parse_plpgsql_body(&body_text) {
5327                Ok(block) => FunctionBody::PlPgSql(block),
5328                // Best-effort: if the body parser doesn't yet
5329                // support a construct used inside, fall back to
5330                // raw — keeps `CREATE FUNCTION` itself working
5331                // (catalogue accepts), executor errors on
5332                // invocation only.
5333                Err(_) => FunctionBody::Raw(body_text),
5334            }
5335        } else {
5336            FunctionBody::Raw(body_text)
5337        };
5338        Ok(Statement::CreateFunction(CreateFunctionStatement {
5339            name,
5340            or_replace,
5341            args,
5342            returns,
5343            language,
5344            body,
5345            attrs,
5346        }))
5347    }
5348
5349    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5350    /// attribute clauses into `attrs`, stopping at the first token that
5351    /// is not one. Measured against PG 18.4, which accepts them in any
5352    /// order and on either side of the body.
5353    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5354        loop {
5355            let word = match self.peek() {
5356                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5357                // NOT LEAKPROOF — NOT is a reserved keyword token.
5358                Token::Not
5359                    if matches!(
5360                        self.tokens.get(self.pos + 1),
5361                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5362                    ) =>
5363                {
5364                    self.advance();
5365                    self.advance();
5366                    attrs.leakproof = false;
5367                    continue;
5368                }
5369                _ => return Ok(()),
5370            };
5371            match word.as_str() {
5372                "immutable" => {
5373                    self.advance();
5374                    attrs.volatility = FunctionVolatility::Immutable;
5375                }
5376                "stable" => {
5377                    self.advance();
5378                    attrs.volatility = FunctionVolatility::Stable;
5379                }
5380                "volatile" => {
5381                    self.advance();
5382                    attrs.volatility = FunctionVolatility::Volatile;
5383                }
5384                "strict" => {
5385                    self.advance();
5386                    attrs.strict = true;
5387                }
5388                "leakproof" => {
5389                    self.advance();
5390                    attrs.leakproof = true;
5391                }
5392                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5393                // spelled-out forms of STRICT and its opposite.
5394                "returns" | "called" => {
5395                    let strict = word == "returns";
5396                    let mut probe = self.pos + 1;
5397                    if strict {
5398                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5399                        // is not ours.
5400                        match self.tokens.get(probe) {
5401                            Some(Token::Null) => probe += 1,
5402                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5403                            _ => return Ok(()),
5404                        }
5405                    }
5406                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5407                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5408                    if !ok {
5409                        return Ok(());
5410                    }
5411                    probe += 1;
5412                    match self.tokens.get(probe) {
5413                        Some(Token::Null) => probe += 1,
5414                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5415                        _ => return Ok(()),
5416                    }
5417                    match self.tokens.get(probe) {
5418                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5419                        _ => return Ok(()),
5420                    }
5421                    self.pos = probe;
5422                    attrs.strict = strict;
5423                }
5424                "security" | "external" => {
5425                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5426                    let mut probe = self.pos + 1;
5427                    if word == "external" {
5428                        match self.tokens.get(probe) {
5429                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5430                                probe += 1;
5431                            }
5432                            _ => return Ok(()),
5433                        }
5434                    }
5435                    let definer = match self.tokens.get(probe) {
5436                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5437                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5438                        _ => return Ok(()),
5439                    };
5440                    self.pos = probe + 1;
5441                    attrs.security_definer = definer;
5442                }
5443                "parallel" => {
5444                    let level = match self.tokens.get(self.pos + 1) {
5445                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5446                            FunctionParallel::Safe
5447                        }
5448                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5449                            FunctionParallel::Restricted
5450                        }
5451                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5452                            FunctionParallel::Unsafe
5453                        }
5454                        _ => return Ok(()),
5455                    };
5456                    self.pos += 2;
5457                    attrs.parallel = level;
5458                }
5459                "cost" | "rows" => {
5460                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5461                        return Ok(());
5462                    };
5463                    self.pos += 2;
5464                    if word == "cost" {
5465                        attrs.cost = Some(n);
5466                    } else {
5467                        attrs.rows = Some(n);
5468                    }
5469                }
5470                _ => return Ok(()),
5471            }
5472        }
5473    }
5474
5475    /// The numeric literal at `idx`, if there is one.
5476    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5477        match self.tokens.get(idx)? {
5478            Token::Integer(n) => Some(*n as f64),
5479            Token::Float(f) => Some(*f),
5480            Token::Numeric(t) => t.parse::<f64>().ok(),
5481            _ => None,
5482        }
5483    }
5484
5485    /// Closing `)`-terminated argument list. v7.12.4 commonly
5486    /// sees the empty `()`; typed args round-trip but the
5487    /// executor (yet) doesn't invoke them.
5488    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5489    /// it away, which is what PG does with one on a function parameter.
5490    fn skip_type_modifier(&mut self) {
5491        if !matches!(self.peek(), Token::LParen) {
5492            return;
5493        }
5494        // Only a numeric modifier — anything else is not one, and eating
5495        // it would swallow real grammar.
5496        let mut i = self.pos + 1;
5497        let mut seen_number = false;
5498        loop {
5499            match self.tokens.get(i) {
5500                Some(Token::Integer(_)) => seen_number = true,
5501                Some(Token::Comma) => {}
5502                Some(Token::RParen) => break,
5503                _ => return,
5504            }
5505            i += 1;
5506        }
5507        if !seen_number {
5508            return;
5509        }
5510        while self.pos <= i {
5511            self.advance();
5512        }
5513    }
5514
5515    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5516        let mut args: Vec<FunctionArg> = Vec::new();
5517        if matches!(self.peek(), Token::RParen) {
5518            self.advance();
5519            return Ok(args);
5520        }
5521        loop {
5522            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5523            // a reserved token; OUT / INOUT are bare idents.
5524            let mode = if matches!(self.peek(), Token::In) {
5525                self.advance();
5526                FunctionArgMode::In
5527            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5528            {
5529                self.advance();
5530                FunctionArgMode::Out
5531            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5532            {
5533                self.advance();
5534                FunctionArgMode::InOut
5535            } else {
5536                FunctionArgMode::In
5537            };
5538            // Optional name. The next token is either a name
5539            // (followed by a type ident) or the type itself.
5540            // Disambiguate by peeking ahead: if the token after
5541            // the next ident is also an ident, we treat the
5542            // first as the name.
5543            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5544            // the comma or paren, then decide. Reading at most two of
5545            // them could not spell `x double precision` at all, and
5546            // silently mis-read the bare `double precision` as a
5547            // parameter named "double" — which is what made the same
5548            // signature key two different ways.
5549            let (name, ty_token) = {
5550                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5551                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5552                    words.push(self.expect_ident_like()?);
5553                }
5554                // v7.39 (round 344) — a length / precision modifier on the
5555                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5556                // accepts it and DROPS it — `pg_get_function_arguments`
5557                // reports plain `character varying` / `numeric`, measured on
5558                // 18.4 — but SPG raised `syntax error at or near "("`,
5559                // because the modifier's parens were never consumed.
5560                self.skip_type_modifier();
5561                // r1049 — `f(v bigint[])`. The array suffix parsed in
5562                // the column position, the cast position and (r1038)
5563                // the RETURNS position, but not here: the fifth
5564                // member of the same family, reported by sentori as
5565                // presumably the same code. It is now.
5566                let array_suffix = self.consume_array_suffix();
5567                let whole = words.join(" ");
5568                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5569                {
5570                    (Some(words[0].clone()), words[1..].join(" "))
5571                } else {
5572                    (None, whole)
5573                };
5574                ty_token.push_str(&array_suffix);
5575                (name, ty_token)
5576            };
5577            // Type — try to map to ColumnTypeName, else Raw.
5578            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5579                Some(t) => FunctionArgType::Typed(t),
5580                None => FunctionArgType::Raw(ty_token),
5581            };
5582            args.push(FunctionArg { mode, name, ty });
5583            match self.peek() {
5584                Token::Comma => {
5585                    self.advance();
5586                    continue;
5587                }
5588                Token::RParen => {
5589                    self.advance();
5590                    return Ok(args);
5591                }
5592                other => {
5593                    return Err(self.err(alloc::format!(
5594                        "expected , or ) in function arg list, got {other:?}"
5595                    )));
5596                }
5597            }
5598        }
5599    }
5600
5601    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5602        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5603        // function whose row shape is named inline.
5604        if matches!(self.peek(), Token::Table)
5605            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5606        {
5607            self.advance(); // TABLE
5608            self.advance(); // (
5609            let mut cols: Vec<String> = Vec::new();
5610            loop {
5611                let cname = self.expect_ident_like()?;
5612                let mut ty: Vec<String> = Vec::new();
5613                loop {
5614                    match self.peek() {
5615                        Token::Comma | Token::RParen | Token::Eof => break,
5616                        _ => {}
5617                    }
5618                    match self.advance() {
5619                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5620                        other => {
5621                            if let Some(w) = unreserved_keyword_text(&other) {
5622                                ty.push(w);
5623                            }
5624                        }
5625                    }
5626                }
5627                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5628                if matches!(self.peek(), Token::Comma) {
5629                    self.advance();
5630                } else {
5631                    break;
5632                }
5633            }
5634            if matches!(self.peek(), Token::RParen) {
5635                self.advance();
5636            }
5637            return Ok(FunctionReturn::Other(alloc::format!(
5638                "TABLE({})",
5639                cols.join(", ")
5640            )));
5641        }
5642        let ident = self.expect_ident_like()?;
5643        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5644        if ident.eq_ignore_ascii_case("setof") {
5645            let inner = self.expect_ident_like()?;
5646            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5647            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5648        }
5649        if ident.eq_ignore_ascii_case("trigger") {
5650            return Ok(FunctionReturn::Trigger);
5651        }
5652        if ident.eq_ignore_ascii_case("void") {
5653            return Ok(FunctionReturn::Void);
5654        }
5655        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5656        // RETURN position did not, so the `[` was a syntax error and the
5657        // whole migration stopped. sentori worked around it by returning
5658        // zero-padded text.
5659        let suffix = self.consume_array_suffix();
5660        if !suffix.is_empty() {
5661            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5662        }
5663        match map_type_ident_to_column_type_name(&ident) {
5664            Some(t) => Ok(FunctionReturn::Type(t)),
5665            None => Ok(FunctionReturn::Other(ident)),
5666        }
5667    }
5668
5669    /// Consume any `[]` / `[N]` array markers after a type name and give
5670    /// back their text. Empty when there are none.
5671    fn consume_array_suffix(&mut self) -> String {
5672        let mut out = String::new();
5673        while matches!(self.peek(), Token::LBracket) {
5674            self.advance();
5675            // `[N]` is accepted and, as in PG, the length is not enforced.
5676            if let Token::Integer(n) = self.peek().clone() {
5677                self.advance();
5678                out.push_str(&alloc::format!("[{n}]"));
5679            } else {
5680                out.push_str("[]");
5681            }
5682            if matches!(self.peek(), Token::RBracket) {
5683                self.advance();
5684            }
5685        }
5686        out
5687    }
5688
5689    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5690        match self.peek() {
5691            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5692                self.advance();
5693                let lang = self.expect_ident_like()?;
5694                Ok(Some(lang.to_ascii_lowercase()))
5695            }
5696            _ => Ok(None),
5697        }
5698    }
5699
5700    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5701    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5702    /// (expr)]*`. The `DOMAIN` keyword has already been
5703    /// consumed. PG allows the trailing constraints in any
5704    /// order; we approximate with a small loop.
5705    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5706        let name = self.expect_ident_like()?;
5707        // Optional `AS`.
5708        if matches!(self.peek(), Token::As) {
5709            self.advance();
5710        }
5711        // v7.39 (round 259) — keep the raw type NAME when the base is not
5712        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5713        // parent domain.
5714        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5715            self.parse_type_with_implied_flags()?;
5716        let mut default: Option<Expr> = None;
5717        let mut not_null = false;
5718        let mut checks: Vec<Expr> = Vec::new();
5719        loop {
5720            match self.peek() {
5721                Token::Default => {
5722                    if default.is_some() {
5723                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5724                    }
5725                    self.advance();
5726                    default = Some(self.parse_expr(0)?);
5727                }
5728                Token::Not => {
5729                    self.advance();
5730                    if !matches!(self.peek(), Token::Null) {
5731                        return Err(self.err(alloc::format!(
5732                            "expected NULL after NOT in DOMAIN, got {:?}",
5733                            self.peek()
5734                        )));
5735                    }
5736                    self.advance();
5737                    not_null = true;
5738                }
5739                Token::Null => {
5740                    self.advance();
5741                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5742                    // is the default-nullable marker (PG accepts it),
5743                    // but AFTER a NOT NULL it is a conflict PG refuses
5744                    // (`conflicting NULL/NOT NULL constraints`,
5745                    // PG18-measured); the old arm no-opped both ways.
5746                    if not_null {
5747                        return Err(self.err(alloc::string::String::from(
5748                            "conflicting NULL/NOT NULL constraints",
5749                        )));
5750                    }
5751                }
5752                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5753                    self.advance();
5754                    if !matches!(self.peek(), Token::LParen) {
5755                        return Err(self.err(alloc::format!(
5756                            "expected '(' after CHECK in DOMAIN, got {:?}",
5757                            self.peek()
5758                        )));
5759                    }
5760                    self.advance();
5761                    let expr = self.parse_expr(0)?;
5762                    if !matches!(self.peek(), Token::RParen) {
5763                        return Err(self.err(alloc::format!(
5764                            "expected ')' after CHECK expr, got {:?}",
5765                            self.peek()
5766                        )));
5767                    }
5768                    self.advance();
5769                    checks.push(expr);
5770                }
5771                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5772                // prefix on the constraint; we drop the name and
5773                // recurse into the constraint parsing.
5774                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5775                    self.advance();
5776                    let _ = self.expect_ident_like()?;
5777                }
5778                _ => break,
5779            }
5780        }
5781        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5782            name,
5783            base_type,
5784            base_domain: base_user_ref,
5785            default,
5786            not_null,
5787            checks,
5788        }))
5789    }
5790
5791    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5792    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5793    /// consumed.
5794    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5795        let name = self.expect_ident_like()?;
5796        // Required `AS`.
5797        if !matches!(self.peek(), Token::As) {
5798            return Err(self.err(alloc::format!(
5799                "expected AS after CREATE TYPE {name:?}, got {:?}",
5800                self.peek()
5801            )));
5802        }
5803        self.advance();
5804        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5805        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5806        // on the next token: `(` = composite, ident `ENUM` = enum.
5807        if matches!(self.peek(), Token::LParen) {
5808            self.advance();
5809            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5810            let mut field_user_types: Vec<Option<String>> = Vec::new();
5811            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5812            // is legal PG (an attribute-less composite; measured — the old
5813            // e2e note claimed PG requires at least one attribute).
5814            if matches!(self.peek(), Token::RParen) {
5815                self.advance();
5816                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5817                    name,
5818                    kind: crate::ast::TypeKind::Composite {
5819                        fields,
5820                        field_user_types,
5821                    },
5822                }));
5823            }
5824            loop {
5825                let field_name = self.expect_ident_like()?;
5826                // v7.39 (round 264) — keep the raw type name when it is not
5827                // a builtin: that is how a NESTED composite field records
5828                // which composite it holds.
5829                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5830                    self.parse_type_with_implied_flags()?;
5831                fields.push((field_name, field_type));
5832                field_user_types.push(field_user_ref);
5833                if matches!(self.peek(), Token::Comma) {
5834                    self.advance();
5835                    continue;
5836                }
5837                if matches!(self.peek(), Token::RParen) {
5838                    self.advance();
5839                    break;
5840                }
5841                return Err(self.err(alloc::format!(
5842                    "expected , or ) in composite field list, got {:?}",
5843                    self.peek()
5844                )));
5845            }
5846            if fields.is_empty() {
5847                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5848            }
5849            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5850                name,
5851                kind: crate::ast::TypeKind::Composite {
5852                    fields,
5853                    field_user_types,
5854                },
5855            }));
5856        }
5857        // Required `ENUM` ident.
5858        let kind_ident = match self.peek().clone() {
5859            Token::Ident(s) | Token::QuotedIdent(s) => s,
5860            other => {
5861                return Err(self.err(alloc::format!(
5862                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5863                )));
5864            }
5865        };
5866        if !kind_ident.eq_ignore_ascii_case("enum") {
5867            return Err(self.err(alloc::format!(
5868                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5869            )));
5870        }
5871        self.advance();
5872        if !matches!(self.peek(), Token::LParen) {
5873            return Err(self.err(alloc::format!(
5874                "expected '(' after ENUM, got {:?}",
5875                self.peek()
5876            )));
5877        }
5878        self.advance();
5879        let mut labels: Vec<String> = Vec::new();
5880        loop {
5881            match self.peek().clone() {
5882                Token::String(s) => {
5883                    self.advance();
5884                    labels.push(s);
5885                }
5886                other => {
5887                    return Err(
5888                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5889                    );
5890                }
5891            }
5892            if matches!(self.peek(), Token::Comma) {
5893                self.advance();
5894                continue;
5895            }
5896            if matches!(self.peek(), Token::RParen) {
5897                self.advance();
5898                break;
5899            }
5900            return Err(self.err(alloc::format!(
5901                "expected , or ) in ENUM label list, got {:?}",
5902                self.peek()
5903            )));
5904        }
5905        if labels.is_empty() {
5906            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5907        }
5908        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5909            name,
5910            kind: crate::ast::TypeKind::Enum { labels },
5911        }))
5912    }
5913
5914    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5915    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5916    /// The `CREATE MATERIALIZED VIEW` keywords have already been
5917    /// consumed.
5918    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5919        let if_not_exists = self.parse_if_not_exists();
5920        let name = self.expect_ident_like()?;
5921        let mut columns: Vec<String> = Vec::new();
5922        if matches!(self.peek(), Token::LParen) {
5923            self.advance();
5924            loop {
5925                let c = self.expect_ident_like()?;
5926                columns.push(c);
5927                if matches!(self.peek(), Token::Comma) {
5928                    self.advance();
5929                    continue;
5930                }
5931                if matches!(self.peek(), Token::RParen) {
5932                    self.advance();
5933                    break;
5934                }
5935                return Err(self.err(alloc::format!(
5936                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5937                    self.peek()
5938                )));
5939            }
5940        }
5941        if !matches!(self.peek(), Token::As) {
5942            return Err(self.err(alloc::format!(
5943                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5944                self.peek()
5945            )));
5946        }
5947        self.advance();
5948        // v7.39 (round 151) — a WITH-headed body is legal (read-only
5949        // CTEs only; the engine rejects data-modifying ones with PG's
5950        // message). A trailing `WITH [NO] DATA` can't START the body,
5951        // so WITH here heads the query.
5952        let body = if self.peek_is_with_kw() {
5953            self.advance();
5954            self.parse_nested_with_select()?
5955        } else {
5956            let body_stmt = self.parse_select_stmt()?;
5957            let Statement::Select(body) = body_stmt else {
5958                return Err(self.err(alloc::format!(
5959                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5960                )));
5961            };
5962            body
5963        };
5964        // Optional trailing `WITH [NO] DATA`.
5965        let with_data = self.parse_optional_with_data(true)?;
5966        Ok(Statement::CreateMaterializedView(
5967            crate::ast::CreateMaterializedViewStatement {
5968                temporary: false,
5969                name,
5970                if_not_exists,
5971                columns,
5972                body,
5973                with_data,
5974                as_plain_table: false,
5975            },
5976        ))
5977    }
5978
5979    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5980    /// `default_when_absent` is what to return if the tail is
5981    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5982    /// WITH DATA).
5983    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5984        let save = self.pos;
5985        // `WITH` is an Ident (not reserved in the lexer).
5986        let is_with = match self.peek() {
5987            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5988            _ => false,
5989        };
5990        if !is_with {
5991            return Ok(default_when_absent);
5992        }
5993        self.advance();
5994        // Optional `NO`.
5995        let mut with_data = true;
5996        let is_no = match self.peek() {
5997            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5998            _ => false,
5999        };
6000        if is_no {
6001            self.advance();
6002            with_data = false;
6003        }
6004        // Required `DATA` ident.
6005        let is_data = match self.peek() {
6006            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6007            _ => false,
6008        };
6009        if is_data {
6010            self.advance();
6011            Ok(with_data)
6012        } else {
6013            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6014            // parser can interpret it.
6015            self.pos = save;
6016            Ok(default_when_absent)
6017        }
6018    }
6019
6020    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6021    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6022    /// All keyword prefixes have already been consumed; the flags
6023    /// say which were present.
6024    fn parse_create_view_after_keyword(
6025        &mut self,
6026        or_replace: bool,
6027        _materialized_unused: bool,
6028        temporary: bool,
6029    ) -> Result<Statement, ParseError> {
6030        let if_not_exists = self.parse_if_not_exists();
6031        let name = self.expect_ident_like()?;
6032        // Optional `(col, col, …)` rename list.
6033        let mut columns: Vec<String> = Vec::new();
6034        if matches!(self.peek(), Token::LParen) {
6035            self.advance();
6036            loop {
6037                let c = self.expect_ident_like()?;
6038                columns.push(c);
6039                if matches!(self.peek(), Token::Comma) {
6040                    self.advance();
6041                    continue;
6042                }
6043                if matches!(self.peek(), Token::RParen) {
6044                    self.advance();
6045                    break;
6046                }
6047                return Err(self.err(alloc::format!(
6048                    "expected , or ) in VIEW column list, got {:?}",
6049                    self.peek()
6050                )));
6051            }
6052        }
6053        // Required `AS`.
6054        if !matches!(self.peek(), Token::As) {
6055            return Err(self.err(alloc::format!(
6056                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6057                self.peek()
6058            )));
6059        }
6060        self.advance();
6061        // Body: a regular SELECT statement. v7.39 (round 151) — a
6062        // WITH-headed body is legal too (read-only CTEs only; the
6063        // engine rejects data-modifying ones with PG's message).
6064        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6065        // with the check-option clause, so WITH here heads the query.
6066        let body = if self.peek_is_with_kw() {
6067            self.advance();
6068            self.parse_nested_with_select()?
6069        } else {
6070            let body_stmt = self.parse_select_stmt()?;
6071            let Statement::Select(body) = body_stmt else {
6072                return Err(self.err(alloc::format!(
6073                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6074                )));
6075            };
6076            body
6077        };
6078        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6079        // The SELECT parser stops before a trailing WITH, so it lands here.
6080        let check_option = if matches!(self.peek(),
6081            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6082        {
6083            self.advance(); // WITH
6084            let opt = match self.peek() {
6085                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6086                    self.advance();
6087                    crate::ast::ViewCheckOption::Local
6088                }
6089                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6090                    self.advance();
6091                    crate::ast::ViewCheckOption::Cascaded
6092                }
6093                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6094                _ => crate::ast::ViewCheckOption::Cascaded,
6095            };
6096            if !matches!(self.peek(),
6097                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6098            {
6099                return Err(self.err(alloc::format!(
6100                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6101                    self.peek()
6102                )));
6103            }
6104            self.advance(); // CHECK
6105            if !matches!(self.peek(),
6106                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6107            {
6108                return Err(self.err(alloc::format!(
6109                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6110                    self.peek()
6111                )));
6112            }
6113            self.advance(); // OPTION
6114            Some(opt)
6115        } else {
6116            None
6117        };
6118        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6119            name,
6120            or_replace,
6121            if_not_exists,
6122            temporary,
6123            columns,
6124            body,
6125            check_option,
6126        }))
6127    }
6128
6129    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6130    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6131    /// consumed; `temporary` carries whether TEMPORARY was seen.
6132    fn parse_create_sequence_after_keyword(
6133        &mut self,
6134        temporary: bool,
6135    ) -> Result<Statement, ParseError> {
6136        let if_not_exists = self.parse_if_not_exists();
6137        let name = self.expect_ident_like()?;
6138        // Optional `AS data_type`.
6139        let data_type = if matches!(self.peek(), Token::As) {
6140            self.advance();
6141            Some(self.parse_sequence_data_type()?)
6142        } else {
6143            None
6144        };
6145        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6146        Ok(Statement::CreateSequence(
6147            crate::ast::CreateSequenceStatement {
6148                name,
6149                if_not_exists,
6150                temporary,
6151                data_type,
6152                options,
6153            },
6154        ))
6155    }
6156
6157    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6158    /// already been consumed; this is reached after `SEQUENCE`.
6159    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6160    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6161        use crate::ast::AlterDomainAction as A;
6162        let name = self.expect_ident_like()?;
6163        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6164        let kw = match self.peek() {
6165            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6166            Token::Drop => alloc::string::String::from("drop"),
6167            Token::Default => alloc::string::String::from("default"),
6168            other => {
6169                return Err(self.err(alloc::format!(
6170                    "expected an ALTER DOMAIN action, got {other:?}"
6171                )));
6172            }
6173        };
6174        let action = match kw.as_str() {
6175            "add" => {
6176                self.advance();
6177                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6178                {
6179                    self.advance();
6180                    Some(self.expect_ident_like()?)
6181                } else {
6182                    None
6183                };
6184                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6185                    return Err(self.err(alloc::format!(
6186                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6187                        self.peek()
6188                    )));
6189                }
6190                self.advance();
6191                if !matches!(self.peek(), Token::LParen) {
6192                    return Err(self.err("expected '(' after CHECK".into()));
6193                }
6194                self.advance();
6195                let check = self.parse_expr(0)?;
6196                if !matches!(self.peek(), Token::RParen) {
6197                    return Err(self.err("expected ')' after CHECK expression".into()));
6198                }
6199                self.advance();
6200                A::AddConstraint { name: cname, check }
6201            }
6202            "drop" => {
6203                self.advance();
6204                match self.peek() {
6205                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6206                        self.advance();
6207                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6208                        {
6209                            self.advance();
6210                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6211                            {
6212                                return Err(self.err("expected EXISTS after IF".into()));
6213                            }
6214                            self.advance();
6215                            true
6216                        } else {
6217                            false
6218                        };
6219                        let cn = self.expect_ident_like()?;
6220                        A::DropConstraint {
6221                            name: cn,
6222                            if_exists,
6223                        }
6224                    }
6225                    Token::Default => {
6226                        self.advance();
6227                        A::DropDefault
6228                    }
6229                    Token::Not => {
6230                        self.advance();
6231                        if !matches!(self.peek(), Token::Null) {
6232                            return Err(self.err("expected NULL after NOT".into()));
6233                        }
6234                        self.advance();
6235                        A::DropNotNull
6236                    }
6237                    other => {
6238                        return Err(self.err(alloc::format!(
6239                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6240                        )));
6241                    }
6242                }
6243            }
6244            "set" => {
6245                self.advance();
6246                match self.peek() {
6247                    Token::Default => {
6248                        self.advance();
6249                        A::SetDefault(self.parse_expr(0)?)
6250                    }
6251                    Token::Not => {
6252                        self.advance();
6253                        if !matches!(self.peek(), Token::Null) {
6254                            return Err(self.err("expected NULL after NOT".into()));
6255                        }
6256                        self.advance();
6257                        A::SetNotNull
6258                    }
6259                    other => {
6260                        return Err(self.err(alloc::format!(
6261                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6262                        )));
6263                    }
6264                }
6265            }
6266            "rename" => {
6267                self.advance();
6268                if !matches!(self.peek(), Token::To) {
6269                    return Err(self.err("expected TO after RENAME".into()));
6270                }
6271                self.advance();
6272                A::RenameTo(self.expect_ident_like()?)
6273            }
6274            other => {
6275                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6276            }
6277        };
6278        Ok(Statement::AlterDomain { name, action })
6279    }
6280
6281    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6282        let if_exists = self.parse_if_exists();
6283        let name = self.expect_ident_like()?;
6284        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6285        // the option list (PG allows only one or the other).
6286        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6287            self.advance();
6288            if matches!(self.peek(), Token::To) {
6289                self.advance();
6290            } else {
6291                self.expect_keyword_ident("to")?;
6292            }
6293            let new = self.expect_ident_like()?;
6294            return Ok(Statement::AlterSequence(
6295                crate::ast::AlterSequenceStatement {
6296                    name,
6297                    if_exists,
6298                    options: crate::ast::SequenceOptions::default(),
6299                    rename_to: Some(new),
6300                },
6301            ));
6302        }
6303        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6304        Ok(Statement::AlterSequence(
6305            crate::ast::AlterSequenceStatement {
6306                name,
6307                if_exists,
6308                options,
6309                rename_to: None,
6310            },
6311        ))
6312    }
6313
6314    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6315        let kw = self.expect_ident_like()?;
6316        match kw.to_ascii_lowercase().as_str() {
6317            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6318            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6319            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6320            other => Err(self.err(alloc::format!(
6321                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6322            ))),
6323        }
6324    }
6325
6326    fn parse_sequence_options(
6327        &mut self,
6328        allow_restart: bool,
6329    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6330        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6331        let mut opts = SequenceOptions::default();
6332        #[allow(clippy::while_let_loop)]
6333        loop {
6334            // Match an ident; stop at any non-ident token (sentinel,
6335            // semicolon, end of statement).
6336            let kw_lc = match self.peek() {
6337                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6338                _ => break,
6339            };
6340            match kw_lc.as_str() {
6341                "increment" => {
6342                    self.advance();
6343                    // Optional BY.
6344                    if self.peek_is_by() {
6345                        self.advance();
6346                    }
6347                    opts.increment = Some(self.expect_signed_int()?);
6348                }
6349                "minvalue" => {
6350                    self.advance();
6351                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6352                }
6353                "maxvalue" => {
6354                    self.advance();
6355                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6356                }
6357                "no" => {
6358                    self.advance();
6359                    let what = self.expect_ident_like()?;
6360                    match what.to_ascii_lowercase().as_str() {
6361                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6362                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6363                        "cycle" => opts.cycle = Some(false),
6364                        other => {
6365                            return Err(self.err(alloc::format!(
6366                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6367                            )));
6368                        }
6369                    }
6370                }
6371                "start" => {
6372                    self.advance();
6373                    // Optional WITH.
6374                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6375                        if s.eq_ignore_ascii_case("with"))
6376                    {
6377                        self.advance();
6378                    }
6379                    opts.start = Some(self.expect_signed_int()?);
6380                }
6381                "restart" if allow_restart => {
6382                    self.advance();
6383                    // Optional WITH n; bare RESTART means restart at START.
6384                    let mut with_val: Option<i64> = None;
6385                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6386                        if s.eq_ignore_ascii_case("with"))
6387                    {
6388                        self.advance();
6389                        with_val = Some(self.expect_signed_int()?);
6390                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6391                        with_val = Some(self.expect_signed_int()?);
6392                    }
6393                    opts.restart = Some(with_val);
6394                }
6395                "cache" => {
6396                    self.advance();
6397                    opts.cache = Some(self.expect_signed_int()?);
6398                }
6399                "cycle" => {
6400                    self.advance();
6401                    opts.cycle = Some(true);
6402                }
6403                "owned" => {
6404                    self.advance();
6405                    match self.peek() {
6406                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6407                            self.advance();
6408                        }
6409                        other => {
6410                            return Err(
6411                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6412                            );
6413                        }
6414                    }
6415                    // OWNED BY {NONE | tab.col}. Read just one ident
6416                    // (NOT expect_ident_like which would auto-strip
6417                    // a schema prefix and consume the `.col` we need).
6418                    let first = match self.advance() {
6419                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6420                        other => {
6421                            return Err(self.err(alloc::format!(
6422                                "expected identifier or NONE after OWNED BY, got {other:?}"
6423                            )));
6424                        }
6425                    };
6426                    if first.eq_ignore_ascii_case("none") {
6427                        opts.owned_by = Some(SequenceOwnedBy::None);
6428                    } else if matches!(self.peek(), Token::Dot) {
6429                        self.advance();
6430                        let second = match self.advance() {
6431                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6432                            other => {
6433                                return Err(self.err(alloc::format!(
6434                                    "expected column name after OWNED BY {first}., got {other:?}"
6435                                )));
6436                            }
6437                        };
6438                        // v7.17 dump-compat fix — pg_dump emits
6439                        // OWNED BY clauses as
6440                        // `schema.table.column` (three segments).
6441                        // If a third `.<ident>` follows, treat the
6442                        // first ident as schema (drop it; SPG is
6443                        // single-schema) and the middle / last
6444                        // pair as table.column. Otherwise it's
6445                        // the two-segment form table.column.
6446                        if matches!(self.peek(), Token::Dot) {
6447                            self.advance();
6448                            let third = match self.advance() {
6449                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6450                                other => {
6451                                    return Err(self.err(alloc::format!(
6452                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6453                                    )));
6454                                }
6455                            };
6456                            let _ = first; // schema prefix discarded
6457                            opts.owned_by = Some(SequenceOwnedBy::Column {
6458                                table: second,
6459                                column: third,
6460                            });
6461                        } else {
6462                            opts.owned_by = Some(SequenceOwnedBy::Column {
6463                                table: first,
6464                                column: second,
6465                            });
6466                        }
6467                    } else {
6468                        return Err(self.err(alloc::format!(
6469                            "expected table.column or NONE after OWNED BY, got {first:?}"
6470                        )));
6471                    }
6472                }
6473                _ => break,
6474            }
6475        }
6476        Ok(opts)
6477    }
6478
6479    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6480        let neg = if matches!(self.peek(), Token::Minus) {
6481            self.advance();
6482            true
6483        } else {
6484            false
6485        };
6486        match self.peek() {
6487            Token::Integer(n) => {
6488                let v = *n;
6489                self.advance();
6490                Ok(if neg { -v } else { v })
6491            }
6492            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6493        }
6494    }
6495
6496    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6497    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6498    /// clause is fully accepted and discarded — SPG always runs
6499    /// constraint checks immediately (single-writer model). The
6500    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6501    /// in either order (per the SQL spec they're independent),
6502    /// though pg_dump always emits them in the canonical
6503    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6504    /// Stops at the first token that isn't part of the clause.
6505    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6506        self.consume_deferrable_clauses_timed().map(|_| ())
6507    }
6508
6509    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6510    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6511    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6512    /// NOT DEFERRABLE and a circular-FK migration could not load.
6513    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6514        let mut deferrable = false;
6515        let mut initially_deferred = false;
6516        loop {
6517            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6518            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6519                self.advance();
6520                deferrable = true;
6521                if self.consume_optional_initially_clause()? {
6522                    initially_deferred = true;
6523                }
6524                continue;
6525            }
6526            // `NOT DEFERRABLE` — already worked pre-3.1.
6527            if matches!(self.peek(), Token::Not) {
6528                let look = self.tokens.get(self.pos + 1);
6529                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6530                    self.advance(); // NOT
6531                    self.advance(); // DEFERRABLE
6532                    deferrable = false;
6533                    initially_deferred = false;
6534                    let _ = self.consume_optional_initially_clause()?;
6535                    continue;
6536                }
6537                break;
6538            }
6539            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6540            // accepts this without a leading [NOT] DEFERRABLE
6541            // (the timing keyword alone). pg_dump occasionally
6542            // emits it on FK constraints that inherit timing.
6543            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6544                if self.consume_optional_initially_clause()? {
6545                    initially_deferred = true;
6546                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6547                    deferrable = true;
6548                }
6549                continue;
6550            }
6551            break;
6552        }
6553        Ok((deferrable, initially_deferred))
6554    }
6555
6556    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6557    /// next token is `INITIALLY`, consume it plus the required
6558    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6559    /// Returns true when the timing seen was `DEFERRED`.
6560    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6561        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6562            return Ok(false);
6563        }
6564        self.advance(); // INITIALLY
6565        match self.advance() {
6566            Token::Ident(s)
6567                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6568            {
6569                Ok(s.eq_ignore_ascii_case("deferred"))
6570            }
6571            other => Err(self.err(alloc::format!(
6572                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6573            ))),
6574        }
6575    }
6576
6577    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6578    /// in its entirety so the parser returns Empty without
6579    /// touching the runtime. The CREATE+PROCEDURE keywords are
6580    /// already consumed; this swallows everything from the
6581    /// procedure name through the matching `END`, including
6582    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6583    /// (DELIMITER `//` makes the script splitter forward the
6584    /// whole block as one statement), `@var` session-variable
6585    /// references, and the trailing terminator.
6586    ///
6587    /// Tracks nesting depth so:
6588    ///   BEGIN
6589    ///     IF cond THEN
6590    ///       BEGIN ... END;
6591    ///     END IF;
6592    ///   END
6593    /// terminates at the outer END.
6594    fn consume_mysql_routine_body(&mut self) {
6595        // Outer skeleton: name, (...), optional clauses, BEGIN
6596        // <body> END [;]. Scan for the first BEGIN — anything
6597        // before it is signature decoration we don't care about.
6598        // Once inside BEGIN, count up on BEGIN, down on END.
6599        let mut depth: i32 = 0;
6600        let mut started = false;
6601        loop {
6602            match self.peek().clone() {
6603                Token::Begin => {
6604                    self.advance();
6605                    depth += 1;
6606                    started = true;
6607                }
6608                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6609                    self.advance();
6610                    if started {
6611                        depth -= 1;
6612                        if depth <= 0 {
6613                            // Optional trailing ident (`END IF`,
6614                            // `END LOOP`, `END WHILE`, `END CASE`,
6615                            // `END label_name`) — eat the next
6616                            // ident if present so we don't
6617                            // mistake `END IF;` for the outer
6618                            // close.
6619                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6620                                // If the next token is one of the
6621                                // PL/SQL block-closer keywords,
6622                                // the END belongs to an inner
6623                                // block; bump depth back up.
6624                                let is_inner_close = matches!(
6625                                    self.peek(),
6626                                    Token::Ident(s) | Token::QuotedIdent(s)
6627                                        if matches!(
6628                                            s.to_ascii_lowercase().as_str(),
6629                                            "if" | "loop" | "while" | "case" | "repeat"
6630                                        )
6631                                );
6632                                if is_inner_close {
6633                                    self.advance();
6634                                    depth += 1;
6635                                    continue;
6636                                }
6637                            }
6638                            // Eat optional trailing `;`.
6639                            if matches!(self.peek(), Token::Semicolon) {
6640                                self.advance();
6641                            }
6642                            return;
6643                        }
6644                    }
6645                }
6646                Token::Eof => return,
6647                _ => {
6648                    self.advance();
6649                }
6650            }
6651        }
6652    }
6653
6654    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6655    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6656    ///
6657    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6658    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6659    ///   ident, or `ident @ ident-or-quoted-string` host form)
6660    /// * `SQL SECURITY {DEFINER|INVOKER}`
6661    ///
6662    /// Each clause may appear at most once but in any order.
6663    /// The hints are pure planner / permission metadata that
6664    /// SPG's view-rewrite engine handles uniformly; we accept
6665    /// and discard. Returns `Ok(())` once a non-clause token is
6666    /// peeked (the caller then checks for the `VIEW` keyword).
6667    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6668        loop {
6669            match self.peek().clone() {
6670                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6671                    self.advance(); // ALGORITHM
6672                    // Optional `=`. MySQL spec requires it but be
6673                    // generous.
6674                    if matches!(self.peek(), Token::Eq) {
6675                        self.advance();
6676                    }
6677                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6678                    // bare ident; unknown values still parse so
6679                    // future MySQL versions don't break.
6680                    if matches!(
6681                        self.peek(),
6682                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6683                    ) {
6684                        self.advance();
6685                    }
6686                }
6687                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6688                    self.advance(); // DEFINER
6689                    if matches!(self.peek(), Token::Eq) {
6690                        self.advance();
6691                    }
6692                    // User: quoted string, ident, OR ident @ host
6693                    // (host may itself be quoted or bare).
6694                    match self.peek().clone() {
6695                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6696                            self.advance();
6697                            // Optional `@host`.
6698                            if matches!(self.peek(), Token::At) {
6699                                self.advance();
6700                                if matches!(
6701                                    self.peek(),
6702                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6703                                ) {
6704                                    self.advance();
6705                                }
6706                            }
6707                        }
6708                        _ => {}
6709                    }
6710                }
6711                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6712                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6713                    // when followed by SECURITY — the dispatcher must
6714                    // not consume a bare `SQL` token (it's not a
6715                    // legal CREATE prefix on its own).
6716                    let save = self.pos;
6717                    self.advance(); // SQL
6718                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6719                        if s2.eq_ignore_ascii_case("security"))
6720                    {
6721                        self.advance(); // SECURITY
6722                        // DEFINER / INVOKER trailing ident.
6723                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6724                            self.advance();
6725                        }
6726                    } else {
6727                        // Not a SQL SECURITY clause — roll back and
6728                        // bail; the caller will error out cleanly.
6729                        self.pos = save;
6730                        return Ok(());
6731                    }
6732                }
6733                _ => return Ok(()),
6734            }
6735        }
6736    }
6737
6738    fn parse_if_not_exists(&mut self) -> bool {
6739        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6740        {
6741            let save = self.pos;
6742            self.advance();
6743            if matches!(self.peek(), Token::Not) {
6744                self.advance();
6745                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6746                {
6747                    self.advance();
6748                    return true;
6749                }
6750            }
6751            self.pos = save;
6752        }
6753        false
6754    }
6755
6756    fn parse_if_exists(&mut self) -> bool {
6757        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6758        {
6759            let save = self.pos;
6760            self.advance();
6761            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6762            {
6763                self.advance();
6764                return true;
6765            }
6766            self.pos = save;
6767        }
6768        false
6769    }
6770
6771    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6772    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6773    /// been consumed.
6774    fn parse_create_trigger_after_keyword(
6775        &mut self,
6776        or_replace: bool,
6777    ) -> Result<Statement, ParseError> {
6778        let name = self.expect_ident_like()?;
6779        let timing = {
6780            let ident = self.expect_ident_like()?;
6781            if ident.eq_ignore_ascii_case("before") {
6782                TriggerTiming::Before
6783            } else if ident.eq_ignore_ascii_case("after") {
6784                TriggerTiming::After
6785            } else if ident.eq_ignore_ascii_case("instead") {
6786                let next = self.expect_ident_like()?;
6787                if !next.eq_ignore_ascii_case("of") {
6788                    return Err(self.err(alloc::format!(
6789                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6790                    )));
6791                }
6792                TriggerTiming::InsteadOf
6793            } else {
6794                return Err(self.err(alloc::format!(
6795                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6796                )));
6797            }
6798        };
6799        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6800        // OR is a reserved keyword token (Token::Or), not an Ident.
6801        // v7.13.0 — after an UPDATE event we may optionally see
6802        // `OF col, col, …` (mailrs round-5 G7). Columns are
6803        // captured into `update_columns` once across the whole
6804        // events list; multiple `UPDATE OF` clauses are rejected.
6805        let mut events: Vec<TriggerEvent> = Vec::new();
6806        let mut update_columns: Vec<String> = Vec::new();
6807        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6808        events.push(first_ev);
6809        if !first_cols.is_empty() {
6810            update_columns = first_cols;
6811        }
6812        while matches!(self.peek(), Token::Or) {
6813            self.advance();
6814            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6815            events.push(ev);
6816            if !cols.is_empty() {
6817                if !update_columns.is_empty() {
6818                    return Err(
6819                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6820                    );
6821                }
6822                update_columns = cols;
6823            }
6824        }
6825        // ON <table>
6826        let tok = self.peek();
6827        let Token::On = tok else {
6828            return Err(self.err(alloc::format!(
6829                "expected ON after trigger events, got {tok:?}"
6830            )));
6831        };
6832        self.advance();
6833        let table = self.expect_ident_like()?;
6834        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6835        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6836        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6837        // the trigger as a plain AFTER trigger (correct for every non-deferred
6838        // use; deferral timing is not yet honoured).
6839        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6840            if s.eq_ignore_ascii_case("from"))
6841        {
6842            self.advance();
6843            let _reftable = self.expect_ident_like()?;
6844        }
6845        self.consume_optional_deferrable_clauses()?;
6846        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6847        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6848        // idents.
6849        if !matches!(self.peek(), Token::For) {
6850            return Err(self.err(alloc::format!(
6851                "expected FOR EACH ROW / STATEMENT, got {:?}",
6852                self.peek()
6853            )));
6854        }
6855        self.advance();
6856        let for_each = {
6857            let e = self.expect_ident_like()?;
6858            if !e.eq_ignore_ascii_case("each") {
6859                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6860            }
6861            let unit = self.expect_ident_like()?;
6862            if unit.eq_ignore_ascii_case("row") {
6863                TriggerForEach::Row
6864            } else if unit.eq_ignore_ascii_case("statement") {
6865                TriggerForEach::Statement
6866            } else {
6867                return Err(self.err(alloc::format!(
6868                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6869                )));
6870            }
6871        };
6872        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6873        let when_condition = if matches!(self.peek(),
6874            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6875        {
6876            self.advance();
6877            Some(self.parse_paren_expr("WHEN")?)
6878        } else {
6879            None
6880        };
6881        // EXECUTE FUNCTION/PROCEDURE name(...)
6882        let exec = self.expect_ident_like()?;
6883        if !exec.eq_ignore_ascii_case("execute") {
6884            return Err(self.err(alloc::format!(
6885                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6886            )));
6887        }
6888        let fn_or_proc = self.expect_ident_like()?;
6889        if !(fn_or_proc.eq_ignore_ascii_case("function")
6890            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6891        {
6892            return Err(self.err(alloc::format!(
6893                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6894            )));
6895        }
6896        let function = self.expect_ident_like()?;
6897        // Optional empty arg list `()`.
6898        if matches!(self.peek(), Token::LParen) {
6899            self.advance();
6900            if !matches!(self.peek(), Token::RParen) {
6901                return Err(self.err(alloc::format!(
6902                    "v7.12.4 trigger function calls take no args; got {:?}",
6903                    self.peek()
6904                )));
6905            }
6906            self.advance();
6907        }
6908        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6909            name,
6910            or_replace,
6911            timing,
6912            events,
6913            table,
6914            for_each,
6915            function,
6916            update_columns,
6917            when_condition,
6918        }))
6919    }
6920
6921    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6922    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6923    fn parse_create_rule_after_keyword(
6924        &mut self,
6925        or_replace: bool,
6926    ) -> Result<Statement, ParseError> {
6927        let name = self.expect_ident_like()?;
6928        if !matches!(self.peek(), Token::As) {
6929            return Err(self.err(alloc::format!(
6930                "expected AS in CREATE RULE, got {:?}",
6931                self.peek()
6932            )));
6933        }
6934        self.advance();
6935        if !matches!(self.peek(), Token::On) {
6936            return Err(self.err(alloc::format!(
6937                "expected ON in CREATE RULE, got {:?}",
6938                self.peek()
6939            )));
6940        }
6941        self.advance();
6942        let event = self.parse_rule_event()?;
6943        if !matches!(self.peek(), Token::To)
6944            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6945        {
6946            return Err(self.err(alloc::format!(
6947                "expected TO after rule event, got {:?}",
6948                self.peek()
6949            )));
6950        }
6951        self.advance();
6952        let table = self.expect_ident_like()?;
6953        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6954        let when_condition = if matches!(self.peek(), Token::Where) {
6955            self.advance();
6956            Some(self.parse_expr(0)?)
6957        } else {
6958            None
6959        };
6960        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6961        {
6962            return Err(self.err(alloc::format!(
6963                "expected DO in CREATE RULE, got {:?}",
6964                self.peek()
6965            )));
6966        }
6967        self.advance();
6968        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6969        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6970        {
6971            self.advance();
6972            true
6973        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6974            self.advance();
6975            false
6976        } else {
6977            false
6978        };
6979        // `NOTHING` | `( cmd; … )` | `cmd`.
6980        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6981        {
6982            self.advance();
6983            Vec::new()
6984        } else if matches!(self.peek(), Token::LParen) {
6985            self.advance();
6986            let mut cmds = Vec::new();
6987            loop {
6988                cmds.push(self.parse_one_statement()?);
6989                if matches!(self.peek(), Token::Semicolon) {
6990                    self.advance();
6991                    if matches!(self.peek(), Token::RParen) {
6992                        break;
6993                    }
6994                    continue;
6995                }
6996                break;
6997            }
6998            if !matches!(self.peek(), Token::RParen) {
6999                return Err(self.err(alloc::format!(
7000                    "expected ) closing the CREATE RULE command list, got {:?}",
7001                    self.peek()
7002                )));
7003            }
7004            self.advance();
7005            cmds
7006        } else {
7007            alloc::vec![self.parse_one_statement()?]
7008        };
7009        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7010            name,
7011            or_replace,
7012            event,
7013            table,
7014            instead,
7015            when_condition,
7016            commands,
7017        }))
7018    }
7019
7020    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7021    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7022        if matches!(self.peek(), Token::Insert) {
7023            self.advance();
7024            return Ok(alloc::string::String::from("INSERT"));
7025        }
7026        if matches!(self.peek(), Token::Select) {
7027            self.advance();
7028            return Ok(alloc::string::String::from("SELECT"));
7029        }
7030        match self.peek() {
7031            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7032                self.advance();
7033                Ok(alloc::string::String::from("UPDATE"))
7034            }
7035            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7036                self.advance();
7037                Ok(alloc::string::String::from("DELETE"))
7038            }
7039            other => Err(self.err(alloc::format!(
7040                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7041            ))),
7042        }
7043    }
7044
7045    /// v7.13.0 — parse one trigger event, then optionally consume
7046    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7047    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7048    fn parse_trigger_event_with_optional_of(
7049        &mut self,
7050    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7051        let ev = self.parse_trigger_event()?;
7052        if !matches!(ev, TriggerEvent::Update) {
7053            return Ok((ev, Vec::new()));
7054        }
7055        // `OF` is a bare ident.
7056        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7057            return Ok((ev, Vec::new()));
7058        }
7059        self.advance(); // OF
7060        let mut cols: Vec<String> = Vec::new();
7061        loop {
7062            cols.push(self.expect_ident_like()?);
7063            if matches!(self.peek(), Token::Comma) {
7064                self.advance();
7065                continue;
7066            }
7067            break;
7068        }
7069        if cols.is_empty() {
7070            return Err(
7071                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7072            );
7073        }
7074        Ok((ev, cols))
7075    }
7076
7077    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7078    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7079    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7080    /// inside the body.
7081    /// Called by [`parse_plpgsql_body`] after the body's tokens
7082    /// have been lexed into this temporary parser.
7083    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7084        // v7.12.6 — optional DECLARE prelude.
7085        let declarations = if matches!(
7086            self.peek(),
7087            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7088        ) {
7089            self.advance();
7090            self.parse_plpgsql_declare_block()?
7091        } else {
7092            Vec::new()
7093        };
7094        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7095        // `BEGIN` transaction-start, but we can reuse the
7096        // reserved Token::Begin since the body is a separate
7097        // lex/parse context).
7098        if !matches!(self.peek(), Token::Begin) {
7099            return Err(self.err(alloc::format!(
7100                "expected BEGIN at start of plpgsql block, got {:?}",
7101                self.peek()
7102            )));
7103        }
7104        self.advance();
7105        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7106        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7107        // body's last statement and the trailing END. When present
7108        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7109        // arms terminated by END.
7110        let exception_handlers = if matches!(
7111            self.peek(),
7112            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7113        ) {
7114            self.advance();
7115            self.parse_plpgsql_exception_handlers()?
7116        } else {
7117            Vec::new()
7118        };
7119        Ok(PlPgSqlBlock {
7120            declarations,
7121            statements,
7122            exception_handlers,
7123        })
7124    }
7125
7126    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7127    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7128    fn parse_plpgsql_exception_handlers(
7129        &mut self,
7130    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7131        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7132        loop {
7133            // Stop at END — the block-level trailing END LOOP / END;
7134            // is handled by the caller.
7135            if matches!(
7136                self.peek(),
7137                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7138            ) {
7139                return Ok(out);
7140            }
7141            // WHEN <cond> [OR <cond>]* THEN <body>
7142            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7143            {
7144                return Err(self.err(alloc::format!(
7145                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7146                    self.peek()
7147                )));
7148            }
7149            self.advance();
7150            let mut conditions: Vec<String> = Vec::new();
7151            conditions.push(self.expect_ident_like()?);
7152            while matches!(self.peek(), Token::Or) {
7153                self.advance();
7154                conditions.push(self.expect_ident_like()?);
7155            }
7156            let then_kw = self.expect_ident_like()?;
7157            if !then_kw.eq_ignore_ascii_case("then") {
7158                return Err(self.err(alloc::format!(
7159                    "expected THEN after WHEN condition list, got {then_kw:?}"
7160                )));
7161            }
7162            let body = self.parse_plpgsql_stmt_list_until_end()?;
7163            out.push(crate::ast::ExceptionHandler { conditions, body });
7164        }
7165    }
7166
7167    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7168    /// prelude. Caller has already consumed `DECLARE`. We stop
7169    /// reading entries when we hit `BEGIN`.
7170    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7171        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7172        loop {
7173            if matches!(self.peek(), Token::Begin) {
7174                return Ok(out);
7175            }
7176            let name = self.expect_ident_like()?;
7177            // v7.37.20 (20.7) — type inference: if the next token is
7178            // `:=` or `=` (no explicit type), infer from the default
7179            // expression. Otherwise the ident that follows is the
7180            // declared type.
7181            //
7182            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7183            // (PG-standard). SPG parse-accepts and treats identically
7184            // to inference — the eventual runtime value determines
7185            // the local's type, which is faithful to how SPG handles
7186            // untyped locals today (see 20.7). Full compile-time
7187            // catalog lookup queues with v7.40 PL/pgSQL epic.
7188            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7189                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7190                // downstream declaration walker to type the local by
7191                // the runtime type of the default expression.
7192                FunctionArgType::Raw("_infer_".into())
7193            } else {
7194                let ty_token = self.expect_ident_like()?;
7195                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7196                // consume optional `.<ident>` qualifier + `%<KW>`
7197                // suffix. Both qualifier and suffix map to _infer_.
7198                if matches!(self.peek(), Token::Dot) {
7199                    self.advance();
7200                    let _ = self.expect_ident_like()?;
7201                }
7202                if matches!(self.peek(), Token::Percent) {
7203                    self.advance();
7204                    // Consume the trailing TYPE / ROWTYPE ident.
7205                    let _ = self.expect_ident_like()?;
7206                    FunctionArgType::Raw("_infer_".into())
7207                } else {
7208                    match map_type_ident_to_column_type_name(&ty_token) {
7209                        Some(t) => FunctionArgType::Typed(t),
7210                        None => FunctionArgType::Raw(ty_token),
7211                    }
7212                }
7213            };
7214            let default = match self.peek() {
7215                Token::ColonEq => {
7216                    self.advance();
7217                    Some(self.parse_expr(0)?)
7218                }
7219                Token::Eq => {
7220                    // PL/pgSQL also accepts `=` for the
7221                    // DECLARE default (PG treats them the same
7222                    // in this position).
7223                    self.advance();
7224                    Some(self.parse_expr(0)?)
7225                }
7226                _ => None,
7227            };
7228            // Mandatory `;` between declarations.
7229            if !matches!(self.peek(), Token::Semicolon) {
7230                return Err(self.err(alloc::format!(
7231                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7232                    self.peek()
7233                )));
7234            }
7235            self.advance();
7236            out.push(PlPgSqlDeclare { name, ty, default });
7237        }
7238    }
7239
7240    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7241    /// the terminating `END;` (or `END IF;` etc — handled by the
7242    /// per-construct sub-parsers). Used by both the outer block
7243    /// and the IF/ELSE branch bodies.
7244    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7245        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7246        loop {
7247            // Allow trailing semicolons + END.
7248            while matches!(self.peek(), Token::Semicolon) {
7249                self.advance();
7250            }
7251            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7252            if matches!(
7253                self.peek(),
7254                Token::Ident(s) | Token::QuotedIdent(s)
7255                    if s.eq_ignore_ascii_case("end")
7256                        || s.eq_ignore_ascii_case("else")
7257                        || s.eq_ignore_ascii_case("elsif")
7258                        || s.eq_ignore_ascii_case("elseif")
7259                        || s.eq_ignore_ascii_case("exception")
7260                        || s.eq_ignore_ascii_case("when")
7261            ) {
7262                return Ok(statements);
7263            }
7264            // Otherwise: one statement, then expect `;` or
7265            // a block-terminator keyword.
7266            let stmt = self.parse_plpgsql_stmt()?;
7267            statements.push(stmt);
7268            match self.peek() {
7269                Token::Semicolon => {
7270                    self.advance();
7271                }
7272                Token::Ident(s) | Token::QuotedIdent(s)
7273                    if s.eq_ignore_ascii_case("end")
7274                        || s.eq_ignore_ascii_case("else")
7275                        || s.eq_ignore_ascii_case("elsif")
7276                        || s.eq_ignore_ascii_case("elseif")
7277                        || s.eq_ignore_ascii_case("exception")
7278                        || s.eq_ignore_ascii_case("when") =>
7279                {
7280                    // Final statement of the block without `;`.
7281                }
7282                other => {
7283                    return Err(self.err(alloc::format!(
7284                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7285                    )));
7286                }
7287            }
7288        }
7289    }
7290
7291    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7292        // RETURN keyword?
7293        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7294        {
7295            self.advance();
7296            return self.parse_plpgsql_return();
7297        }
7298        // v7.12.6 — IF block.
7299        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7300        {
7301            self.advance();
7302            return self.parse_plpgsql_if();
7303        }
7304        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7305        // Detected by peeking that token pos+3 is Ident("execute").
7306        if matches!(self.peek(), Token::For)
7307            && matches!(
7308                self.tokens.get(self.pos + 1),
7309                Some(Token::Ident(_) | Token::QuotedIdent(_))
7310            )
7311            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7312            && matches!(
7313                self.tokens.get(self.pos + 3),
7314                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7315            )
7316        {
7317            self.advance(); // FOR
7318            let var = self.expect_ident_like()?;
7319            self.advance(); // IN
7320            self.advance(); // EXECUTE
7321            // Prescan for LOOP at paren depth 0 so parse_expr stops
7322            // before the LOOP keyword (same trick as the bare-SELECT
7323            // ForQuery arm).
7324            let mut depth: i32 = 0;
7325            let mut loop_pos: Option<usize> = None;
7326            let mut scan = self.pos;
7327            while scan < self.tokens.len() {
7328                match self.tokens.get(scan) {
7329                    Some(Token::LParen) => depth += 1,
7330                    Some(Token::RParen) => depth -= 1,
7331                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7332                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7333                    {
7334                        loop_pos = Some(scan);
7335                        break;
7336                    }
7337                    _ => {}
7338                }
7339                scan += 1;
7340            }
7341            let loop_pos = loop_pos.ok_or_else(|| {
7342                self.err(alloc::format!(
7343                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7344                ))
7345            })?;
7346            let saved_loop = self.tokens[loop_pos].clone();
7347            self.tokens[loop_pos] = Token::Semicolon;
7348            let expr_result = self.parse_expr(0);
7349            self.tokens[loop_pos] = saved_loop;
7350            let sql_expr = expr_result?;
7351            let loop_kw = self.expect_ident_like()?;
7352            if !loop_kw.eq_ignore_ascii_case("loop") {
7353                return Err(self.err(alloc::format!(
7354                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7355                )));
7356            }
7357            let body = self.parse_plpgsql_stmt_list_until_end()?;
7358            let end_kw = self.expect_ident_like()?;
7359            if !end_kw.eq_ignore_ascii_case("end") {
7360                return Err(self.err(alloc::format!(
7361                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7362                )));
7363            }
7364            let loop_kw2 = self.expect_ident_like()?;
7365            if !loop_kw2.eq_ignore_ascii_case("loop") {
7366                return Err(self.err(alloc::format!(
7367                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7368                )));
7369            }
7370            return Ok(PlPgSqlStmt::ForExecute {
7371                var,
7372                sql_expr,
7373                body,
7374            });
7375        }
7376        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7377        //
7378        // Two syntactic forms:
7379        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7380        //   FOR var IN (SELECT ...) LOOP ...
7381        //
7382        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7383        // the trailing `LOOP` keyword as a table alias, we prescan
7384        // forward to find LOOP at paren depth 0, splice a fake
7385        // Semicolon at that position (so SELECT parses cleanly),
7386        // then re-splice LOOP back in.
7387        //
7388        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7389        // LOOP directly — no scan required.
7390        if matches!(self.peek(), Token::For)
7391            && matches!(
7392                self.tokens.get(self.pos + 1),
7393                Some(Token::Ident(_) | Token::QuotedIdent(_))
7394            )
7395            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7396            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7397                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7398        {
7399            self.advance(); // FOR
7400            let var = self.expect_ident_like()?;
7401            // IN
7402            self.advance();
7403            let query = if matches!(self.peek(), Token::LParen) {
7404                // Paren-wrapped SELECT.
7405                self.advance();
7406                let inner = self.parse_select_stmt()?;
7407                let Statement::Select(q) = inner else {
7408                    return Err(self.err(alloc::format!(
7409                        "expected SELECT inside (…), got {:?}",
7410                        self.peek()
7411                    )));
7412                };
7413                if !matches!(self.peek(), Token::RParen) {
7414                    return Err(self.err(alloc::format!(
7415                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7416                        self.peek()
7417                    )));
7418                }
7419                self.advance();
7420                q
7421            } else {
7422                // Bare SELECT: prescan to find the LOOP boundary.
7423                let mut depth: i32 = 0;
7424                let mut loop_pos: Option<usize> = None;
7425                let mut scan = self.pos;
7426                while scan < self.tokens.len() {
7427                    match self.tokens.get(scan) {
7428                        Some(Token::LParen) => depth += 1,
7429                        Some(Token::RParen) => depth -= 1,
7430                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7431                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7432                        {
7433                            loop_pos = Some(scan);
7434                            break;
7435                        }
7436                        _ => {}
7437                    }
7438                    scan += 1;
7439                }
7440                let loop_pos = loop_pos.ok_or_else(|| {
7441                    self.err(alloc::format!(
7442                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7443                    ))
7444                })?;
7445                // Swap the LOOP token with a synthetic Semicolon so
7446                // parse_select_stmt stops there, then restore afterward.
7447                let saved_loop = self.tokens[loop_pos].clone();
7448                self.tokens[loop_pos] = Token::Semicolon;
7449                let parse_result = self.parse_select_stmt();
7450                self.tokens[loop_pos] = saved_loop;
7451                let inner = parse_result?;
7452                let Statement::Select(q) = inner else {
7453                    return Err(self.err(alloc::format!(
7454                        "expected SELECT after FOR <var> IN, got {:?}",
7455                        self.peek()
7456                    )));
7457                };
7458                q
7459            };
7460            let loop_kw = self.expect_ident_like()?;
7461            if !loop_kw.eq_ignore_ascii_case("loop") {
7462                return Err(self.err(alloc::format!(
7463                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7464                )));
7465            }
7466            let body = self.parse_plpgsql_stmt_list_until_end()?;
7467            let end_kw = self.expect_ident_like()?;
7468            if !end_kw.eq_ignore_ascii_case("end") {
7469                return Err(self.err(alloc::format!(
7470                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7471                )));
7472            }
7473            let loop_kw2 = self.expect_ident_like()?;
7474            if !loop_kw2.eq_ignore_ascii_case("loop") {
7475                return Err(self.err(alloc::format!(
7476                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7477                )));
7478            }
7479            return Ok(PlPgSqlStmt::ForQuery {
7480                var,
7481                query: Box::new(query),
7482                body,
7483            });
7484        }
7485        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7486        // FOR is a reserved keyword token (Token::For).
7487        if matches!(self.peek(), Token::For)
7488            && matches!(
7489                self.tokens.get(self.pos + 1),
7490                Some(Token::Ident(_) | Token::QuotedIdent(_))
7491            )
7492            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7493        {
7494            self.advance(); // FOR
7495            let var = self.expect_ident_like()?;
7496            if !matches!(self.peek(), Token::In) {
7497                return Err(self.err(alloc::format!(
7498                    "expected IN after FOR <var>, got {:?}",
7499                    self.peek()
7500                )));
7501            }
7502            self.advance();
7503            let reverse = matches!(
7504                self.peek(),
7505                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7506            );
7507            if reverse {
7508                self.advance();
7509            }
7510            let start = self.parse_expr(0)?;
7511            if !matches!(self.peek(), Token::DotDot) {
7512                return Err(self.err(alloc::format!(
7513                    "expected '..' between FOR loop bounds, got {:?}",
7514                    self.peek()
7515                )));
7516            }
7517            self.advance();
7518            let end = self.parse_expr(0)?;
7519            let loop_kw = self.expect_ident_like()?;
7520            if !loop_kw.eq_ignore_ascii_case("loop") {
7521                return Err(self.err(alloc::format!(
7522                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7523                )));
7524            }
7525            let body = self.parse_plpgsql_stmt_list_until_end()?;
7526            let end_kw = self.expect_ident_like()?;
7527            if !end_kw.eq_ignore_ascii_case("end") {
7528                return Err(self.err(alloc::format!(
7529                    "expected END LOOP after FOR body, got {end_kw:?}"
7530                )));
7531            }
7532            let loop_kw2 = self.expect_ident_like()?;
7533            if !loop_kw2.eq_ignore_ascii_case("loop") {
7534                return Err(self.err(alloc::format!(
7535                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7536                )));
7537            }
7538            return Ok(PlPgSqlStmt::ForRange {
7539                var,
7540                start,
7541                end,
7542                reverse,
7543                body,
7544            });
7545        }
7546        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7547        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7548        {
7549            self.advance();
7550            let body = self.parse_plpgsql_stmt_list_until_end()?;
7551            let end_kw = self.expect_ident_like()?;
7552            if !end_kw.eq_ignore_ascii_case("end") {
7553                return Err(self.err(alloc::format!(
7554                    "expected END LOOP after LOOP body, got {end_kw:?}"
7555                )));
7556            }
7557            let loop_kw = self.expect_ident_like()?;
7558            if !loop_kw.eq_ignore_ascii_case("loop") {
7559                return Err(self.err(alloc::format!(
7560                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7561                )));
7562            }
7563            return Ok(PlPgSqlStmt::Loop { body });
7564        }
7565        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7566        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7567        {
7568            self.advance();
7569            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7570            {
7571                self.advance();
7572                Some(self.parse_expr(0)?)
7573            } else {
7574                None
7575            };
7576            return Ok(PlPgSqlStmt::Exit { when });
7577        }
7578        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7579        // already-parsed Statement or a runtime-computed SQL string.
7580        // The disambiguator vs the extended-query-protocol `EXECUTE
7581        // <stmt_name>` (which is a top-level Statement, not a
7582        // plpgsql line) is that inside a DO block / trigger body the
7583        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7584        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7585        {
7586            self.advance();
7587            let sql = self.parse_expr(0)?;
7588            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7589        }
7590        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7591        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7592        {
7593            self.advance();
7594            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7595            {
7596                self.advance();
7597                Some(self.parse_expr(0)?)
7598            } else {
7599                None
7600            };
7601            return Ok(PlPgSqlStmt::Continue { when });
7602        }
7603        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7604        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7605        {
7606            self.advance();
7607            let condition = self.parse_expr(0)?;
7608            let loop_kw = self.expect_ident_like()?;
7609            if !loop_kw.eq_ignore_ascii_case("loop") {
7610                return Err(self.err(alloc::format!(
7611                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7612                )));
7613            }
7614            let body = self.parse_plpgsql_stmt_list_until_end()?;
7615            // Expect END LOOP.
7616            let end_kw = self.expect_ident_like()?;
7617            if !end_kw.eq_ignore_ascii_case("end") {
7618                return Err(self.err(alloc::format!(
7619                    "expected END LOOP after WHILE body, got {end_kw:?}"
7620                )));
7621            }
7622            let loop_kw2 = self.expect_ident_like()?;
7623            if !loop_kw2.eq_ignore_ascii_case("loop") {
7624                return Err(self.err(alloc::format!(
7625                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7626                )));
7627            }
7628            return Ok(PlPgSqlStmt::While { condition, body });
7629        }
7630        // v7.12.6 — RAISE.
7631        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7632        {
7633            self.advance();
7634            return self.parse_plpgsql_raise();
7635        }
7636        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7637        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7638        {
7639            self.advance();
7640            let condition = self.parse_expr(0)?;
7641            let message = if matches!(self.peek(), Token::Comma) {
7642                self.advance();
7643                Some(self.parse_expr(0)?)
7644            } else {
7645                None
7646            };
7647            return Ok(PlPgSqlStmt::Assert { condition, message });
7648        }
7649        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7650        //   "PERFORM is equivalent to SELECT but discards the
7651        //    result." Side effects (function calls, RAISE inside
7652        //    SQL functions, etc.) still execute. We desugar to
7653        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7654        //    existing embedded-statement path handles execution +
7655        //    result-discard cleanly. The result is naturally
7656        //    discarded because EmbeddedSql doesn't propagate row
7657        //    sets back to the plpgsql interpreter.
7658        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7659        {
7660            self.advance();
7661            // Splice a synthetic Token::Select into the stream at
7662            // the current position so parse_select_stmt parses the
7663            // remainder as a normal SELECT body. Token-stream
7664            // surgery mirrors the try_parse_plpgsql_select_into
7665            // pattern used for SELECT … INTO desugaring.
7666            self.tokens.insert(self.pos, Token::Select);
7667            let select = self.parse_select_stmt()?;
7668            let Statement::Select(s) = select else {
7669                return Err(self.err(alloc::format!(
7670                    "expected SELECT body after PERFORM, got {:?}",
7671                    self.peek()
7672                )));
7673            };
7674            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7675        }
7676        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7677        // plpgsql-specific shape (mailrs round-10 migrate-042).
7678        // PG's SELECT INTO at top-level SQL would CREATE a new
7679        // table; inside plpgsql it ASSIGNS the query result to
7680        // a local variable. We detect the INTO at paren-depth
7681        // 0 between SELECT and the statement boundary; if
7682        // found, split the token stream into "pre-INTO
7683        // projection" + "var" + "post-INTO FROM/WHERE…" and
7684        // rebuild as a SelectInto with a regular SELECT body
7685        // (no INTO clause).
7686        if matches!(self.peek(), Token::Select)
7687            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7688        {
7689            return Ok(PlPgSqlStmt::SelectInto {
7690                var: var_name,
7691                body: Box::new(select_body),
7692            });
7693        }
7694        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7695        // SELECT can appear directly inside a trigger body; we
7696        // recurse into the regular Statement parser, which will
7697        // stop at the trailing `;` (which our caller then
7698        // consumes).
7699        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7700        // also embed ALTER / CREATE / DROP statements; route
7701        // those through the same parser so the DO body parses
7702        // cleanly.
7703        if matches!(self.peek(), Token::Insert)
7704            || matches!(self.peek(), Token::Select)
7705            || matches!(self.peek(), Token::Create)
7706            || matches!(self.peek(), Token::Drop)
7707            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7708                if s.eq_ignore_ascii_case("update")
7709                    || s.eq_ignore_ascii_case("delete")
7710                    || s.eq_ignore_ascii_case("alter"))
7711        {
7712            let stmt = self.parse_one_statement()?;
7713            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7714        }
7715        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7716        // followed by `:=` and an expression.
7717        let target = self.parse_plpgsql_assign_target()?;
7718        // PL/pgSQL assignment uses `:=`. The lexer represents
7719        // this as a colon followed by `=`; check both shapes.
7720        match self.peek() {
7721            Token::ColonEq => {
7722                self.advance();
7723            }
7724            Token::Colon => {
7725                self.advance();
7726                if !matches!(self.peek(), Token::Eq) {
7727                    return Err(self.err(alloc::format!(
7728                        "expected := after plpgsql assign target, got `:` then {:?}",
7729                        self.peek()
7730                    )));
7731                }
7732                self.advance();
7733            }
7734            other => {
7735                return Err(self.err(alloc::format!(
7736                    "expected := after plpgsql assign target, got {other:?}"
7737                )));
7738            }
7739        }
7740        let value = self.parse_expr(0)?;
7741        Ok(PlPgSqlStmt::Assign { target, value })
7742    }
7743
7744    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7745    /// [ELSE body] END IF`. `IF` keyword already consumed.
7746    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7747        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7748        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7749        loop {
7750            // <expr> THEN
7751            let cond = self.parse_expr(0)?;
7752            let then_kw = self.expect_ident_like()?;
7753            if !then_kw.eq_ignore_ascii_case("then") {
7754                return Err(self.err(alloc::format!(
7755                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7756                )));
7757            }
7758            let body = self.parse_plpgsql_stmt_list_until_end()?;
7759            branches.push((cond, body));
7760            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7761            match self.peek() {
7762                Token::Ident(s) | Token::QuotedIdent(s)
7763                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7764                {
7765                    self.advance();
7766                    continue;
7767                }
7768                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7769                    self.advance();
7770                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7771                    break;
7772                }
7773                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7774                    break;
7775                }
7776                other => {
7777                    return Err(self.err(alloc::format!(
7778                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7779                    )));
7780                }
7781            }
7782        }
7783        // Expect `END IF` (the END keyword is the one we're
7784        // looking at right now).
7785        let end_kw = self.expect_ident_like()?;
7786        if !end_kw.eq_ignore_ascii_case("end") {
7787            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7788        }
7789        let if_kw = self.expect_ident_like()?;
7790        if !if_kw.eq_ignore_ascii_case("if") {
7791            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7792        }
7793        Ok(PlPgSqlStmt::If {
7794            branches,
7795            else_branch,
7796        })
7797    }
7798
7799    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7800    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7801    /// is already consumed.
7802    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7803        let lvl_ident = self.expect_ident_like()?;
7804        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7805            "notice" => RaiseLevel::Notice,
7806            "warning" => RaiseLevel::Warning,
7807            "info" => RaiseLevel::Info,
7808            "log" => RaiseLevel::Log,
7809            "debug" => RaiseLevel::Debug,
7810            "exception" => RaiseLevel::Exception,
7811            other => {
7812                return Err(self.err(alloc::format!(
7813                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7814                )));
7815            }
7816        };
7817        // Message: required for v7.12.6. PG accepts a bare
7818        // RAISE-rethrow form (no message), reserved for future
7819        // RAISE-no-args support.
7820        let Token::String(msg) = self.peek() else {
7821            return Err(self.err(alloc::format!(
7822                "expected RAISE message string, got {:?}",
7823                self.peek()
7824            )));
7825        };
7826        let message = msg.clone();
7827        self.advance();
7828        // Optional comma-separated args (PG `%` format substitution).
7829        let mut args: Vec<Expr> = Vec::new();
7830        while matches!(self.peek(), Token::Comma) {
7831            self.advance();
7832            args.push(self.parse_expr(0)?);
7833        }
7834        Ok(PlPgSqlStmt::Raise {
7835            level,
7836            message,
7837            args,
7838        })
7839    }
7840
7841    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7842    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7843    /// migrate-042). Returns `(rebuilt_select_without_into,
7844    /// var_name)` when the pattern matches; `None` for
7845    /// regular SELECTs (those go through the embedded-SQL
7846    /// path). Token-stream surgery so the rebuilt SELECT
7847    /// parses through the regular `parse_select_stmt`.
7848    #[allow(clippy::too_many_lines)]
7849    fn try_parse_plpgsql_select_into(
7850        &mut self,
7851    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7852        // Scan forward from `self.pos + 1` (past Token::Select)
7853        // for Token::Into at paren-depth 0, stopping at the
7854        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7855        // end the plpgsql statement.
7856        let start = self.pos;
7857        let mut into_pos: Option<usize> = None;
7858        let mut depth: i32 = 0;
7859        let mut i = start + 1;
7860        while i < self.tokens.len() {
7861            match &self.tokens[i] {
7862                Token::LParen => depth += 1,
7863                Token::RParen => depth -= 1,
7864                Token::Semicolon if depth == 0 => break,
7865                Token::Ident(s)
7866                    if depth == 0
7867                        && (s.eq_ignore_ascii_case("end")
7868                            || s.eq_ignore_ascii_case("else")
7869                            || s.eq_ignore_ascii_case("elsif")) =>
7870                {
7871                    break;
7872                }
7873                Token::Into if depth == 0 => {
7874                    into_pos = Some(i);
7875                    break;
7876                }
7877                _ => {}
7878            }
7879            i += 1;
7880        }
7881        let Some(into_at) = into_pos else {
7882            return Ok(None);
7883        };
7884        // The token immediately after INTO must be the target
7885        // var ident; anything else (e.g. INSERT INTO table)
7886        // ruled out by the depth-0 check above. Capture it.
7887        let var = match self.tokens.get(into_at + 1) {
7888            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7889            other => {
7890                return Err(self.err(alloc::format!(
7891                    "expected variable name after SELECT … INTO, got {other:?}"
7892                )));
7893            }
7894        };
7895        // Find the end of the plpgsql SELECT INTO statement —
7896        // same boundary rules as the depth-0 scan above.
7897        let mut end = into_at + 2;
7898        let mut depth2: i32 = 0;
7899        while end < self.tokens.len() {
7900            match &self.tokens[end] {
7901                Token::LParen => depth2 += 1,
7902                Token::RParen => depth2 -= 1,
7903                Token::Semicolon if depth2 == 0 => break,
7904                Token::Ident(s)
7905                    if depth2 == 0
7906                        && (s.eq_ignore_ascii_case("end")
7907                            || s.eq_ignore_ascii_case("else")
7908                            || s.eq_ignore_ascii_case("elsif")) =>
7909                {
7910                    break;
7911                }
7912                _ => {}
7913            }
7914            end += 1;
7915        }
7916        // Rebuild a token stream that represents the SELECT
7917        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7918        // post-var tokens up to statement end]. Run the
7919        // regular `parse_select_stmt` against it.
7920        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7921        for j in start..into_at {
7922            rebuilt.push(self.tokens[j].clone());
7923        }
7924        for j in (into_at + 2)..end {
7925            rebuilt.push(self.tokens[j].clone());
7926        }
7927        rebuilt.push(Token::Eof);
7928        let saved_pos = self.pos;
7929        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7930        self.pos = 0;
7931        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7932        if !matches!(self.peek(), Token::Select) {
7933            self.tokens = saved_tokens;
7934            self.pos = saved_pos;
7935            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7936        }
7937        let sel = self.parse_select_stmt();
7938        self.tokens = saved_tokens;
7939        self.pos = end;
7940        let sel = sel?;
7941        let Statement::Select(body) = sel else {
7942            return Err(self.err(alloc::format!(
7943                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7944            )));
7945        };
7946        Ok(Some((body, var)))
7947    }
7948
7949    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7950        // v7.16.1 — read the head token DIRECTLY rather than
7951        // via `expect_ident_like`. The v7.14.0 schema-qualifier
7952        // strip (`public.t` → `t`) inside `expect_ident_like`
7953        // greedily consumes any `ident . ident` pair, which
7954        // silently turned every `NEW.col := …` /
7955        // `OLD.col := …` plpgsql assignment into a Local("col")
7956        // assignment — the head "new"/"old" was eaten as if it
7957        // were a schema name and the Dot was consumed too, so
7958        // this function's own `peek() == Token::Dot` check
7959        // below never fired. Every BEFORE trigger that rewrote
7960        // a NEW cell was a silent no-op for two major releases
7961        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7962        // gate failures were investigated as v7.16.1 backlog.
7963        let head = match self.advance() {
7964            Token::Ident(s) | Token::QuotedIdent(s) => s,
7965            other => {
7966                return Err(self.err(alloc::format!(
7967                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7968                )));
7969            }
7970        };
7971        if matches!(self.peek(), Token::Dot) {
7972            self.advance();
7973            let col = self.expect_ident_like()?;
7974            if head.eq_ignore_ascii_case("new") {
7975                return Ok(AssignTarget::NewColumn(col));
7976            }
7977            if head.eq_ignore_ascii_case("old") {
7978                return Ok(AssignTarget::OldColumn(col));
7979            }
7980            return Err(self.err(alloc::format!(
7981                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7982                 got {head:?}.<col>"
7983            )));
7984        }
7985        Ok(AssignTarget::Local(head))
7986    }
7987
7988    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7989        // RETURN NEW / OLD / NULL — bare-ident forms.
7990        match self.peek() {
7991            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7992                self.advance();
7993                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7994            }
7995            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7996                self.advance();
7997                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7998            }
7999            Token::Null => {
8000                self.advance();
8001                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8002            }
8003            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8004            // per PL/pgSQL convention.
8005            Token::Semicolon => {
8006                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8007            }
8008            _ => {}
8009        }
8010        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8011        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8012        // caller-visible effect (blocks don't return sets), so we
8013        // desugar it identically to PERFORM: parse the SELECT (or
8014        // EXECUTE dynamic) as embedded SQL that runs for side
8015        // effects and discards the result. RETURN NEXT <expr>
8016        // (single-row accumulator) queues with v7.40 SETOF function
8017        // infrastructure.
8018        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8019        // and keep going.
8020        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8021        {
8022            self.advance();
8023            let e = self.parse_expr(0)?;
8024            return Ok(PlPgSqlStmt::ReturnNext(e));
8025        }
8026        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8027        {
8028            self.advance();
8029            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8030            // rows go to the set, like the static form. It used to desugar to a
8031            // bare ExecuteDynamic, whose result was DISCARDED.
8032            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8033            {
8034                self.advance();
8035                let sql = self.parse_expr(0)?;
8036                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8037            }
8038            // Bare RETURN QUERY <select>. If the current token is
8039            // not already SELECT (e.g., the user wrote `RETURN QUERY
8040            // <projection> FROM ...` in a shorthand — rare but PG
8041            // accepts a bare projection here), splice one in. Same
8042            // trick as PERFORM.
8043            if !matches!(self.peek(), Token::Select) {
8044                self.tokens.insert(self.pos, Token::Select);
8045            }
8046            let select = self.parse_select_stmt()?;
8047            let Statement::Select(s) = select else {
8048                return Err(self.err(alloc::format!(
8049                    "expected SELECT body after RETURN QUERY, got {:?}",
8050                    self.peek()
8051                )));
8052            };
8053            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8054            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8055            // in a SETOF function is the entire answer thrown away.
8056            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8057        }
8058        // Fall through: parse a full expression.
8059        let e = self.parse_expr(0)?;
8060        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8061    }
8062
8063    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8064        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8065        // are ident-shaped (the parser keys off case-insensitive
8066        // match — same shape used by the top-level Update / Delete
8067        // dispatchers at parse_one_statement).
8068        if matches!(self.peek(), Token::Insert) {
8069            self.advance();
8070            return Ok(TriggerEvent::Insert);
8071        }
8072        match self.peek() {
8073            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8074                self.advance();
8075                Ok(TriggerEvent::Update)
8076            }
8077            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8078                self.advance();
8079                Ok(TriggerEvent::Delete)
8080            }
8081            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8082                self.advance();
8083                Ok(TriggerEvent::Truncate)
8084            }
8085            other => Err(self.err(alloc::format!(
8086                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8087            ))),
8088        }
8089    }
8090
8091    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8092    ///   - (no clause) → implicit `FOR ALL TABLES`
8093    ///   - `FOR ALL TABLES`
8094    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8095    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8096    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8097    ///     REJECTS the bare plural (`invalid publication object list`,
8098    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8099    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8100    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8101        let name = self.expect_ident_or_string()?;
8102        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8103        // shape so existing publications keep parsing identically.
8104        let scope = if matches!(self.peek(), Token::For) {
8105            self.advance();
8106            if matches!(self.peek(), Token::All) {
8107                self.advance();
8108                if !matches!(self.peek(), Token::Tables) {
8109                    return Err(self.err(format!(
8110                        "expected TABLES after FOR ALL, got {:?}",
8111                        self.peek()
8112                    )));
8113                }
8114                self.advance();
8115                if matches!(self.peek(), Token::Except) {
8116                    self.advance();
8117                    let tables = self.parse_publication_table_list()?;
8118                    PublicationScope::AllTablesExcept(tables)
8119                } else {
8120                    PublicationScope::AllTables
8121                }
8122            } else if matches!(self.peek(), Token::Table) {
8123                self.advance();
8124                let tables = self.parse_publication_table_list()?;
8125                PublicationScope::ForTables(tables)
8126            } else if matches!(self.peek(), Token::Tables) {
8127                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8128                // plural (`FOR TABLES t`) is REJECTED (`invalid
8129                // publication object list`); TABLES only pairs with
8130                // `IN SCHEMA`. The old arm accepted it on an
8131                // unverifiable "PG 19 accepts both" claim.
8132                self.advance();
8133                if !matches!(self.peek(), Token::In) {
8134                    return Err(self.err(alloc::string::String::from(
8135                        "invalid publication object list",
8136                    )));
8137                }
8138                self.advance();
8139                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8140                    return Err(self.err(format!(
8141                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8142                        self.peek()
8143                    )));
8144                }
8145                self.advance();
8146                let schema = self.expect_ident_or_string()?;
8147                PublicationScope::TablesInSchema(schema)
8148            } else {
8149                return Err(self.err(format!(
8150                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8151                    self.peek()
8152                )));
8153            }
8154        } else {
8155            PublicationScope::AllTables
8156        };
8157        Ok(Statement::CreatePublication(CreatePublicationStatement {
8158            name,
8159            scope,
8160        }))
8161    }
8162
8163    /// v6.1.3 — Comma-separated identifier list for the publication
8164    /// FOR-clause. Requires at least one entry; empty list is a
8165    /// parse error (PG behaviour). Quoted idents are accepted; the
8166    /// names round-trip through `Display` as `quote_ident(name)`.
8167    ///
8168    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8169    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8170    /// pg_dump output. SPG's publication state today is per-table
8171    /// only (matching the pre-PG-15 surface); the col list + WHERE
8172    /// are parsed so dumps load through and the table name reaches
8173    /// `PublicationScope::ForTables`, but the filter is not enforced
8174    /// at publish time. Re-open when a customer dogfood gate
8175    /// requires per-row-filter or column-subset publish semantics
8176    /// (which gates on persistent slot state landing first, 21.12).
8177    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8178        let first = self.parse_publication_table_entry()?;
8179        let mut out = alloc::vec![first];
8180        while matches!(self.peek(), Token::Comma) {
8181            self.advance();
8182            out.push(self.parse_publication_table_entry()?);
8183        }
8184        Ok(out)
8185    }
8186
8187    /// One table entry inside a FOR TABLE clause:
8188    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8189    /// Returns just the table name; the column list + WHERE predicate
8190    /// are consumed and discarded per the parse-accept-discard
8191    /// commitment above.
8192    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8193        let name = self.expect_ident_like()?;
8194        // Optional column list — `(col, col, …)`.
8195        if matches!(self.peek(), Token::LParen) {
8196            self.advance();
8197            // Empty parens are a PG error too; require ≥ 1 column.
8198            let _ = self.expect_ident_like()?;
8199            while matches!(self.peek(), Token::Comma) {
8200                self.advance();
8201                let _ = self.expect_ident_like()?;
8202            }
8203            if !matches!(self.peek(), Token::RParen) {
8204                return Err(self.err(alloc::format!(
8205                    "expected ')' to close publication column list, got {:?}",
8206                    self.peek()
8207                )));
8208            }
8209            self.advance();
8210        }
8211        // Optional row filter — `WHERE (predicate)`.
8212        if matches!(self.peek(), Token::Where) {
8213            self.advance();
8214            if !matches!(self.peek(), Token::LParen) {
8215                return Err(self.err(alloc::format!(
8216                    "expected '(' after WHERE in publication row filter, got {:?}",
8217                    self.peek()
8218                )));
8219            }
8220            self.advance();
8221            let _ = self.parse_expr(0)?;
8222            if !matches!(self.peek(), Token::RParen) {
8223                return Err(self.err(alloc::format!(
8224                    "expected ')' to close publication WHERE filter, got {:?}",
8225                    self.peek()
8226                )));
8227            }
8228            self.advance();
8229        }
8230        Ok(name)
8231    }
8232
8233    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8234    ///                 CONNECTION '<conn>'
8235    ///                 PUBLICATION <pub> [, <pub> ...]`.
8236    ///
8237    /// The clause order is fixed (CONNECTION first, then
8238    /// PUBLICATION) to match PG. No WITH-options accepted in
8239    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8240    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8241        let name = self.expect_ident_or_string()?;
8242        if !matches!(self.peek(), Token::Connection) {
8243            return Err(self.err(format!(
8244                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8245                self.peek()
8246            )));
8247        }
8248        self.advance();
8249        let conn_str = self.expect_string_literal()?;
8250        if !matches!(self.peek(), Token::Publication) {
8251            return Err(self.err(format!(
8252                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8253                self.peek()
8254            )));
8255        }
8256        self.advance();
8257        // Reuse the publication FOR-list parser shape: at least one
8258        // identifier, comma-separated.
8259        let first = self.expect_ident_like()?;
8260        let mut publications = alloc::vec![first];
8261        while matches!(self.peek(), Token::Comma) {
8262            self.advance();
8263            publications.push(self.expect_ident_like()?);
8264        }
8265        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8266            name,
8267            conn_str,
8268            publications,
8269        }))
8270    }
8271
8272    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8273    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8274    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8275    /// that fit `u64`.
8276    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8277    /// qualifier is a *namespace* the app owns (`app.user_id`,
8278    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8279    /// to discard. So parse the raw segments here instead of
8280    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8281    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8282    /// a single segment and round-trip unchanged.
8283    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8284        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8285        loop {
8286            let seg = match self.advance() {
8287                Token::Ident(s) | Token::QuotedIdent(s) => s,
8288                other if unreserved_keyword_text(&other).is_some() => {
8289                    unreserved_keyword_text(&other).unwrap()
8290                }
8291                other => {
8292                    return Err(ParseError {
8293                        message: format!("expected parameter name, got {other:?}"),
8294                        token_pos: self.consumed_pos(),
8295                    });
8296                }
8297            };
8298            parts.push(seg);
8299            if matches!(self.peek(), Token::Dot) {
8300                self.advance();
8301                continue;
8302            }
8303            break;
8304        }
8305        Ok(parts.join(".").to_ascii_lowercase())
8306    }
8307
8308    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8309        Self::parse_set_value_inner(self)
8310    }
8311
8312    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8313        match self.advance() {
8314            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8315            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8316                Ok(crate::ast::SetValue::Default)
8317            }
8318            Token::Ident(s) | Token::QuotedIdent(s) => {
8319                let mut accum = s;
8320                while matches!(self.peek(), Token::Dot) {
8321                    self.advance();
8322                    let next = self.expect_ident_like()?;
8323                    accum.push('.');
8324                    accum.push_str(&next);
8325                }
8326                Ok(crate::ast::SetValue::Ident(accum))
8327            }
8328            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8329            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8330            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8331            // spellings that lex as keyword tokens, not idents:
8332            // `SET standard_conforming_strings = on` is in every
8333            // pg_dump preamble (`off` already lexes as an ident).
8334            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8335            // DEFAULT lexes as its keyword token, so the ident arm above
8336            // never saw it and the everyday reset form was a syntax error.
8337            Token::Default => Ok(crate::ast::SetValue::Default),
8338            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8339            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8340            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8341            // v7.14.0 — MySQL session/user variable RHS
8342            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8343            // Wrap as Ident so the SET handler can record it; the
8344            // engine treats `@VAR` / `@@VAR` values as opaque
8345            // strings.
8346            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8347            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8348            // is the common MySQL preamble shape. Allow a `+` or
8349            // `-` prefix on negative numerics for parity with PG
8350            // (some param defaults are negative).
8351            Token::Minus => match self.advance() {
8352                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8353                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8354                other => Err(self.err(format!(
8355                    "expected numeric after `-` in SET value, got {other:?}"
8356                ))),
8357            },
8358            other => Err(self.err(format!(
8359                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8360            ))),
8361        }
8362    }
8363
8364    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8365    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8366    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8367    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8368    /// present). Modes are comma-separated per PG; SPG also
8369    /// accepts space-separated for tolerance. READ ONLY / WRITE
8370    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8371    /// surface but not behaviorally honoured today).
8372    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8373    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8374    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8375    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8376    /// session default rather than forcing READ COMMITTED.
8377    fn parse_isolation_level_clauses(
8378        &mut self,
8379    ) -> Result<crate::ast::TransactionModes, ParseError> {
8380        let mut level = IsolationLevel::default();
8381        let mut have_level = false;
8382        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8383        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8384        let mut read_only: Option<bool> = None;
8385        loop {
8386            // ISOLATION LEVEL …
8387            let saw_isolation =
8388                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8389            if saw_isolation {
8390                self.advance(); // ISOLATION
8391                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8392                    return Err(self.err(alloc::format!(
8393                        "expected LEVEL after ISOLATION, got {:?}",
8394                        self.peek()
8395                    )));
8396                }
8397                self.advance(); // LEVEL
8398                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8399                let w1 = self
8400                    .expect_ident_like()
8401                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8402                let lc = w1.to_ascii_lowercase();
8403                level = match lc.as_str() {
8404                    "serializable" => IsolationLevel::Serializable,
8405                    "repeatable" => {
8406                        // Expect READ
8407                        let w2 = self
8408                            .expect_ident_like()
8409                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8410                        if !w2.eq_ignore_ascii_case("read") {
8411                            return Err(self.err(alloc::format!(
8412                                "expected READ after REPEATABLE, got {w2:?}"
8413                            )));
8414                        }
8415                        IsolationLevel::RepeatableRead
8416                    }
8417                    "read" => {
8418                        let w2 = self
8419                            .expect_ident_like()
8420                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8421                        match w2.to_ascii_lowercase().as_str() {
8422                            "committed" => IsolationLevel::ReadCommitted,
8423                            "uncommitted" => IsolationLevel::ReadUncommitted,
8424                            other => {
8425                                return Err(self.err(alloc::format!(
8426                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8427                                )));
8428                            }
8429                        }
8430                    }
8431                    other => {
8432                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8433                    }
8434                };
8435                have_level = true;
8436            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8437                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8438                // read "parsed, not behaviorally honoured", and it was
8439                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8440                // opened an ordinary read-write transaction and accepted
8441                // every write in it.
8442                self.advance();
8443                match self.peek().clone() {
8444                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8445                        self.advance();
8446                        read_only = Some(true);
8447                    }
8448                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8449                        self.advance();
8450                        read_only = Some(false);
8451                    }
8452                    other => {
8453                        return Err(self.err(alloc::format!(
8454                            "expected ONLY or WRITE after READ, got {other:?}"
8455                        )));
8456                    }
8457                }
8458            } else if matches!(self.peek(), Token::Not) {
8459                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8460                self.advance();
8461                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8462                    return Err(self.err(alloc::format!(
8463                        "expected DEFERRABLE after NOT, got {:?}",
8464                        self.peek()
8465                    )));
8466                }
8467                self.advance();
8468            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8469            {
8470                self.advance();
8471            } else {
8472                break;
8473            }
8474            // Optional comma between modes.
8475            if matches!(self.peek(), Token::Comma) {
8476                self.advance();
8477            }
8478        }
8479        Ok(crate::ast::TransactionModes {
8480            isolation: have_level.then_some(level),
8481            read_only,
8482        })
8483    }
8484
8485    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8486        // FOR is a v6.1.2-reserved keyword (Token::For). The
8487        // other two are bare idents — they've never needed lexer
8488        // support and we keep it that way.
8489        if !matches!(self.peek(), Token::For) {
8490            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8491        }
8492        self.advance();
8493        self.expect_keyword_ident("wal")?;
8494        self.expect_keyword_ident("position")?;
8495        let pos = self.expect_u64_literal()?;
8496        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8497        {
8498            self.advance();
8499            self.expect_keyword_ident("timeout")?;
8500            Some(self.expect_u64_literal()?)
8501        } else {
8502            None
8503        };
8504        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8505    }
8506
8507    /// v6.1.7 helper — consume a `Token::Integer` and check it
8508    /// fits `u64`. WAL positions and millisecond timeouts are
8509    /// non-negative.
8510    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8511        match self.advance() {
8512            Token::Integer(n) if n >= 0 => Ok(n as u64),
8513            Token::Integer(n) => Err(ParseError {
8514                message: format!("expected non-negative integer, got {n}"),
8515                token_pos: self.consumed_pos(),
8516            }),
8517            other => Err(ParseError {
8518                message: format!("expected integer literal, got {other:?}"),
8519                token_pos: self.consumed_pos(),
8520            }),
8521        }
8522    }
8523
8524    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8525    /// ROLE '<role>' (defaults to readonly). All string slots accept
8526    /// either a quoted ident or a quoted string literal.
8527    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8528    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8529    ///
8530    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8531    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8532    /// wire role) still parses — it is a different axis from the PG attributes.
8533    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8534    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8535    /// or RESET, so the plain attribute forms keep their old path.
8536    fn peeks_db_role_setting(&self) -> bool {
8537        let mut i = self.pos + 1; // past the object's name
8538        let word = |p: usize| -> Option<String> {
8539            match self.tokens.get(p) {
8540                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8541                Some(Token::In) => Some(String::from("in")),
8542                _ => None,
8543            }
8544        };
8545        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8546            i += 3; // IN DATABASE <name>
8547        }
8548        matches!(word(i).as_deref(), Some("set" | "reset"))
8549    }
8550
8551    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8552        use crate::ast::SetDbRoleSettingStatement;
8553        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8554        // identifier, so the ordinary name reader refuses it. Same trap
8555        // as TABLE / INDEX / FULL / DEFAULT before it.
8556        let name = if matches!(self.peek(), Token::All) {
8557            self.advance();
8558            String::from("all")
8559        } else {
8560            self.expect_ident_or_string()?
8561        };
8562        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8563        let all = name.eq_ignore_ascii_case("all");
8564        let (mut database, mut role) = if is_database {
8565            (Some(name), None)
8566        } else if all {
8567            (None, None)
8568        } else {
8569            (None, Some(name))
8570        };
8571        if matches!(self.peek(), Token::In) {
8572            self.advance();
8573            self.advance(); // DATABASE
8574            database = Some(self.expect_ident_or_string()?);
8575        }
8576        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8577        self.advance(); // SET | RESET
8578        if resetting && matches!(self.peek(), Token::All) {
8579            self.advance();
8580            self.consume_until_statement_boundary();
8581            return Ok(Statement::SetDbRoleSetting(Box::new(
8582                SetDbRoleSettingStatement {
8583                    database,
8584                    role,
8585                    param: None,
8586                    value: None,
8587                },
8588            )));
8589        }
8590        let param = self.expect_ident_like()?;
8591        let value = if resetting {
8592            None
8593        } else {
8594            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8595            // KEYWORD, so the ident-only check missed it and consumed
8596            // the word itself as the value — the same trap as ALL, one
8597            // clause over.
8598            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8599                self.advance();
8600            }
8601            Some(self.take_guc_value())
8602        };
8603        self.consume_until_statement_boundary();
8604        Ok(Statement::SetDbRoleSetting(Box::new(
8605            SetDbRoleSettingStatement {
8606                database,
8607                role,
8608                param: Some(param),
8609                value,
8610            },
8611        )))
8612    }
8613
8614    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8615    /// a quoted literal loses its quotes, a bare word or number does not.
8616    fn take_guc_value(&mut self) -> String {
8617        match self.advance() {
8618            Token::String(s) => s,
8619            Token::Integer(n) => format!("{n}"),
8620            Token::Float(f) => format!("{f}"),
8621            Token::Ident(s) | Token::QuotedIdent(s) => s,
8622            other => format!("{other:?}"),
8623        }
8624    }
8625
8626    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8627        let name = self.expect_ident_or_string()?;
8628        if self.peek_keyword_ident("with") {
8629            self.advance();
8630        }
8631        let mut password = String::new();
8632        let mut role = String::new();
8633        let mut login: Option<bool> = None;
8634        let mut inherit: Option<bool> = None;
8635        let mut superuser: Option<bool> = None;
8636        // Not a `while let`: the pattern would borrow `self` across the
8637        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8638        #[allow(clippy::while_let_loop)]
8639        loop {
8640            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8641                break;
8642            };
8643            match w.to_ascii_lowercase().as_str() {
8644                "password" => {
8645                    self.advance();
8646                    password = self.expect_string_literal()?;
8647                }
8648                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8649                // is the same slot.
8650                "encrypted" => {
8651                    self.advance();
8652                    self.expect_keyword_ident("password")?;
8653                    password = self.expect_string_literal()?;
8654                }
8655                "login" => {
8656                    self.advance();
8657                    login = Some(true);
8658                }
8659                "nologin" => {
8660                    self.advance();
8661                    login = Some(false);
8662                }
8663                "inherit" => {
8664                    self.advance();
8665                    inherit = Some(true);
8666                }
8667                "noinherit" => {
8668                    self.advance();
8669                    inherit = Some(false);
8670                }
8671                "superuser" => {
8672                    self.advance();
8673                    superuser = Some(true);
8674                }
8675                "nosuperuser" => {
8676                    self.advance();
8677                    superuser = Some(false);
8678                }
8679                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8680                "role" => {
8681                    self.advance();
8682                    role = self.expect_string_literal()?;
8683                }
8684                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8685                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8686                // accepted and ignored so a pg_dump role block restores. They
8687                // gate capabilities SPG does not have.
8688                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8689                | "noreplication" | "bypassrls" | "nobypassrls" => {
8690                    self.advance();
8691                }
8692                "connection" => {
8693                    self.advance();
8694                    self.expect_keyword_ident("limit")?;
8695                    self.advance(); // the number
8696                }
8697                "valid" => {
8698                    self.advance();
8699                    self.expect_keyword_ident("until")?;
8700                    self.expect_string_literal()?;
8701                }
8702                _ => break,
8703            }
8704        }
8705        if role.is_empty() {
8706            role = "readonly".to_string();
8707        }
8708        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8709            name,
8710            password,
8711            role,
8712            login,
8713            inherit,
8714            superuser,
8715            is_user,
8716        }))
8717    }
8718
8719    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8720    /// consumed the USING / WITH CHECK keyword.
8721    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8722        if !matches!(self.peek(), Token::LParen) {
8723            return Err(self.err(alloc::format!(
8724                "expected '(' after {clause}, got {:?}",
8725                self.peek()
8726            )));
8727        }
8728        self.advance();
8729        let e = self.parse_expr(0)?;
8730        if !matches!(self.peek(), Token::RParen) {
8731            return Err(self.err(alloc::format!(
8732                "expected ')' to close {clause}, got {:?}",
8733                self.peek()
8734            )));
8735        }
8736        self.advance();
8737        Ok(e)
8738    }
8739
8740    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8741    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8742        let mut roles = Vec::new();
8743        loop {
8744            roles.push(self.expect_ident_like()?);
8745            if matches!(self.peek(), Token::Comma) {
8746                self.advance();
8747            } else {
8748                break;
8749            }
8750        }
8751        Ok(roles)
8752    }
8753
8754    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8755    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8756    /// `CREATE POLICY`.
8757    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8758        use crate::ast::PolicyCmd;
8759        let name = self.expect_ident_like()?;
8760        if !matches!(self.peek(), Token::On) {
8761            return Err(self.err(alloc::format!(
8762                "expected ON after CREATE POLICY name, got {:?}",
8763                self.peek()
8764            )));
8765        }
8766        self.advance();
8767        let table = self.expect_ident_like()?;
8768
8769        let mut permissive = true;
8770        if matches!(self.peek(), Token::As) {
8771            self.advance();
8772            let w = self.expect_ident_like()?;
8773            permissive = if w.eq_ignore_ascii_case("permissive") {
8774                true
8775            } else if w.eq_ignore_ascii_case("restrictive") {
8776                false
8777            } else {
8778                return Err(self.err(alloc::format!(
8779                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8780                )));
8781            };
8782        }
8783
8784        let mut cmd = PolicyCmd::All;
8785        if matches!(self.peek(), Token::For) {
8786            self.advance();
8787            cmd = self.parse_policy_cmd()?;
8788        }
8789
8790        let mut roles = Vec::new();
8791        if matches!(self.peek(), Token::To) {
8792            self.advance();
8793            roles = self.parse_policy_roles()?;
8794        }
8795
8796        let mut using = None;
8797        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8798        {
8799            self.advance();
8800            using = Some(self.parse_paren_expr("USING")?);
8801        }
8802
8803        let mut with_check = None;
8804        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8805        {
8806            self.advance();
8807            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8808            {
8809                return Err(self.err(alloc::format!(
8810                    "expected CHECK after WITH, got {:?}",
8811                    self.peek()
8812                )));
8813            }
8814            self.advance();
8815            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8816        }
8817
8818        // Clause-per-command matrix (PG wording).
8819        match cmd {
8820            PolicyCmd::Insert => {
8821                if using.is_some() {
8822                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8823                }
8824            }
8825            PolicyCmd::Select | PolicyCmd::Delete => {
8826                if with_check.is_some() {
8827                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8828                }
8829            }
8830            PolicyCmd::Update | PolicyCmd::All => {}
8831        }
8832
8833        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8834            name,
8835            table,
8836            permissive,
8837            cmd,
8838            roles,
8839            using,
8840            with_check,
8841        }))
8842    }
8843
8844    /// v7.39 (RLS) — the command word after `FOR`.
8845    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8846        use crate::ast::PolicyCmd;
8847        match self.peek().clone() {
8848            Token::All => {
8849                self.advance();
8850                Ok(PolicyCmd::All)
8851            }
8852            Token::Select => {
8853                self.advance();
8854                Ok(PolicyCmd::Select)
8855            }
8856            Token::Insert => {
8857                self.advance();
8858                Ok(PolicyCmd::Insert)
8859            }
8860            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8861                self.advance();
8862                Ok(PolicyCmd::Update)
8863            }
8864            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8865                self.advance();
8866                Ok(PolicyCmd::Delete)
8867            }
8868            other => Err(self.err(alloc::format!(
8869                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8870            ))),
8871        }
8872    }
8873
8874    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8875    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8876    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8877        let name = self.expect_ident_like()?;
8878        if !matches!(self.peek(), Token::On) {
8879            return Err(self.err(alloc::format!(
8880                "expected ON after ALTER POLICY name, got {:?}",
8881                self.peek()
8882            )));
8883        }
8884        self.advance();
8885        let table = self.expect_ident_like()?;
8886
8887        // RENAME TO new
8888        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8889        {
8890            self.advance();
8891            if !matches!(self.peek(), Token::To) {
8892                return Err(self.err(alloc::format!(
8893                    "expected TO after RENAME, got {:?}",
8894                    self.peek()
8895                )));
8896            }
8897            self.advance();
8898            let new = self.expect_ident_like()?;
8899            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8900                name,
8901                table,
8902                rename_to: Some(new),
8903                roles: None,
8904                using: None,
8905                with_check: None,
8906            }));
8907        }
8908
8909        let mut roles = None;
8910        if matches!(self.peek(), Token::To) {
8911            self.advance();
8912            roles = Some(self.parse_policy_roles()?);
8913        }
8914        let mut using = None;
8915        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8916        {
8917            self.advance();
8918            using = Some(self.parse_paren_expr("USING")?);
8919        }
8920        let mut with_check = None;
8921        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8922        {
8923            self.advance();
8924            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8925            {
8926                return Err(self.err(alloc::format!(
8927                    "expected CHECK after WITH, got {:?}",
8928                    self.peek()
8929                )));
8930            }
8931            self.advance();
8932            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8933        }
8934        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8935            name,
8936            table,
8937            rename_to: None,
8938            roles,
8939            using,
8940            with_check,
8941        }))
8942    }
8943
8944    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8945    /// `DROP POLICY`.
8946    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8947        let if_exists = self.consume_if_exists();
8948        let name = self.expect_ident_like()?;
8949        if !matches!(self.peek(), Token::On) {
8950            return Err(self.err(alloc::format!(
8951                "expected ON after DROP POLICY name, got {:?}",
8952                self.peek()
8953            )));
8954        }
8955        self.advance();
8956        let table = self.expect_ident_like()?;
8957        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8958            name,
8959            table,
8960            if_exists,
8961        }))
8962    }
8963}
8964fn wrap_from_leaves(
8965    e: &mut Expr,
8966    names: &[String],
8967    make: &dyn Fn(Expr) -> Expr,
8968    refs: &dyn Fn(&Expr) -> bool,
8969) {
8970    if let Expr::Column(c) = e {
8971        if c.qualifier
8972            .as_deref()
8973            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8974        {
8975            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8976            *e = make(taken);
8977        }
8978        return;
8979    }
8980    match e {
8981        Expr::Binary { lhs, rhs, .. } => {
8982            wrap_from_leaves(lhs, names, make, refs);
8983            wrap_from_leaves(rhs, names, make, refs);
8984        }
8985        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8986            wrap_from_leaves(expr, names, make, refs)
8987        }
8988        Expr::FunctionCall { args, .. } => {
8989            for a in args.iter_mut() {
8990                wrap_from_leaves(a, names, make, refs);
8991            }
8992        }
8993        Expr::Case {
8994            operand,
8995            branches,
8996            else_branch,
8997        } => {
8998            if let Some(o) = operand.as_deref_mut() {
8999                wrap_from_leaves(o, names, make, refs);
9000            }
9001            for (w, t) in branches.iter_mut() {
9002                wrap_from_leaves(w, names, make, refs);
9003                wrap_from_leaves(t, names, make, refs);
9004            }
9005            if let Some(el) = else_branch.as_deref_mut() {
9006                wrap_from_leaves(el, names, make, refs);
9007            }
9008        }
9009        // Compound variants the walk doesn't decompose: keep the
9010        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9011        // a source table, so nothing regresses.
9012        other => {
9013            if refs(other) {
9014                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9015                *other = make(taken);
9016            }
9017        }
9018    }
9019}
9020
9021/// v7.39 (round 241) — does this expression reference any of the FROM /
9022/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9023/// lowerings)?
9024fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9025    match e {
9026        Expr::Column(c) => c
9027            .qualifier
9028            .as_deref()
9029            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9030        Expr::Binary { lhs, rhs, .. } => {
9031            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9032        }
9033        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9034        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9035        Expr::Case {
9036            operand,
9037            branches,
9038            else_branch,
9039        } => {
9040            operand
9041                .as_deref()
9042                .is_some_and(|o| expr_refs_tables(o, names))
9043                || branches
9044                    .iter()
9045                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9046                || else_branch
9047                    .as_deref()
9048                    .is_some_and(|el| expr_refs_tables(el, names))
9049        }
9050        _ => false,
9051    }
9052}
9053
9054impl Parser {
9055    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9056    /// Caller already consumed the leading `UPDATE` ident.
9057    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9058    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9059    /// after the target name has been read. `JOIN` is a reserved token;
9060    /// the qualifiers are bare idents.
9061    fn peek_is_update_join_start(&self) -> bool {
9062        match self.peek() {
9063            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9064            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9065            Token::Join
9066            | Token::Inner
9067            | Token::Left
9068            | Token::Right
9069            | Token::Cross
9070            | Token::Full => true,
9071            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9072            Token::Ident(s) | Token::QuotedIdent(s) => {
9073                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9074            }
9075            _ => false,
9076        }
9077    }
9078
9079    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9080    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9081    /// expression on the right, and `:=` as a second spelling of `=`.
9082    ///
9083    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9084    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9085    /// and holding this loop's `Vec` + `String` locals there overflowed the
9086    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9087    #[inline(never)]
9088    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9089        let mut assigns: Vec<(String, Expr)> = Vec::new();
9090        let mut settings: Vec<(String, Expr)> = Vec::new();
9091        loop {
9092            // v7.39 (round 554) — a plain NAME here is a session
9093            // setting, not a user variable. mysqldump writes the two in
9094            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9095            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9096            // changes it — and this refused the mixture outright, so no
9097            // dump could be restored past its preamble.
9098            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9099                self.advance();
9100                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9101                    return Err(self.err(alloc::format!(
9102                        "expected `=` after {name}, got {:?}",
9103                        self.peek()
9104                    )));
9105                }
9106                self.advance();
9107                let value = self.parse_expr(0)?;
9108                settings.push((name.to_ascii_lowercase(), value));
9109                if matches!(self.peek(), Token::Comma) {
9110                    self.advance();
9111                    continue;
9112                }
9113                break;
9114            }
9115            let Token::SessionVar(raw) = self.peek().clone() else {
9116                return Err(self.err(alloc::format!(
9117                    "expected a user variable after SET, got {:?}",
9118                    self.peek()
9119                )));
9120            };
9121            if raw.starts_with("@@") {
9122                return Err(self.err(alloc::string::String::from(
9123                    "cannot mix `@@` settings with `@` user variables in one SET",
9124                )));
9125            }
9126            self.advance();
9127            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9128                return Err(self.err(alloc::format!(
9129                    "expected `=` or `:=` after {raw}, got {:?}",
9130                    self.peek()
9131                )));
9132            }
9133            self.advance();
9134            let value = self.parse_expr(0)?;
9135            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9136            if matches!(self.peek(), Token::Comma) {
9137                self.advance();
9138                continue;
9139            }
9140            break;
9141        }
9142        Ok(Statement::SetUserVars(assigns, settings))
9143    }
9144
9145    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9146        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9147        // NAMED `only` until now, which failed on `relation "only" does
9148        // not exist`. The lookahead is what keeps a table actually
9149        // called `only` working: the keyword is only a keyword when a
9150        // TABLE NAME follows it — and `SET` arrives as an identifier
9151        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9152        // for the table and die on the `=`. Measured by the pin.
9153        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9154            if s.eq_ignore_ascii_case("only"))
9155            && matches!(
9156                self.tokens.get(self.pos + 1),
9157                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9158            );
9159        if only {
9160            self.advance();
9161        }
9162        let table = self.expect_ident_like()?;
9163        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9164        // bare spelling; a bare identifier that is the SET keyword itself
9165        // is the clause, not an alias.
9166        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9167        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9168        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9169        // following JOIN a syntax error.
9170        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9171        let alias = if matches!(self.peek(), Token::As) {
9172            self.advance();
9173            Some(self.expect_ident_like()?)
9174        } else {
9175            match self.peek() {
9176                Token::Ident(s) | Token::QuotedIdent(s)
9177                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9178                {
9179                    let a = s.clone();
9180                    self.advance();
9181                    Some(a)
9182                }
9183                _ => None,
9184            }
9185        };
9186        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9187        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9188        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9189        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9190        // The FIRST table is the mutation target and the rest are sources —
9191        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9192        // SPG already lowers onto correlated subqueries. So rewind, let
9193        // `parse_from_clause` read the whole list (it handles aliases, comma
9194        // lists, and every JOIN form), then peel the target off the front.
9195        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9196            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9197        {
9198            // NOTE: `advance()` destroys the tokens it returns
9199            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9200            // is NOT possible — the tail is read forward, once, through the
9201            // same grammar `parse_from_clause` uses after its primary.
9202            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9203            let mut joins = self.parse_from_joins(&target_qual)?;
9204            if joins.is_empty() {
9205                return Err(self.err(alloc::string::String::from(
9206                    "multi-table UPDATE needs at least one source table",
9207                )));
9208            }
9209            let head = joins.remove(0);
9210            // A LEFT join keeps every target row (the unmatched ones see NULL
9211            // on the source side), so it must NOT get the EXISTS row filter
9212            // the inner / comma forms use.
9213            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9214            let src = FromClause {
9215                primary: head.table,
9216                joins,
9217            };
9218            (Some(src), head.on, outer)
9219        } else {
9220            (None, None, false)
9221        };
9222        self.expect_keyword_ident("set")?;
9223        let mut assignments = Vec::new();
9224        loop {
9225            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9226            // …)` — the parenthesized multi-assignment. Expressions
9227            // assign positionally; a subquery RHS clones per column
9228            // keeping only the Nth projection item.
9229            if matches!(self.peek(), Token::LParen) {
9230                self.advance();
9231                let mut cols = alloc::vec![self.expect_ident_like()?];
9232                while matches!(self.peek(), Token::Comma) {
9233                    self.advance();
9234                    cols.push(self.expect_ident_like()?);
9235                }
9236                if !matches!(self.peek(), Token::RParen) {
9237                    return Err(self.err(format!(
9238                        "expected ')' after SET column list, got {:?}",
9239                        self.peek()
9240                    )));
9241                }
9242                self.advance();
9243                if !matches!(self.peek(), Token::Eq) {
9244                    return Err(self.err(format!(
9245                        "expected `=` after SET column list, got {:?}",
9246                        self.peek()
9247                    )));
9248                }
9249                self.advance();
9250                if !matches!(self.peek(), Token::LParen) {
9251                    return Err(self.err(format!(
9252                        "expected '(' after SET (…) =, got {:?}",
9253                        self.peek()
9254                    )));
9255                }
9256                self.advance();
9257                if matches!(self.peek(), Token::Select) {
9258                    let inner = match self.parse_select_stmt()? {
9259                        Statement::Select(s) => s,
9260                        other => {
9261                            return Err(self.err(alloc::format!(
9262                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9263                            )));
9264                        }
9265                    };
9266                    if !matches!(self.peek(), Token::RParen) {
9267                        return Err(self.err(format!(
9268                            "expected ')' after SET subquery, got {:?}",
9269                            self.peek()
9270                        )));
9271                    }
9272                    self.advance();
9273                    if inner.items.len() != cols.len() {
9274                        return Err(self.err(alloc::format!(
9275                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9276                            cols.len(),
9277                            inner.items.len()
9278                        )));
9279                    }
9280                    for (i, col) in cols.into_iter().enumerate() {
9281                        let mut sub = inner.clone();
9282                        sub.items = alloc::vec![sub.items[i].clone()];
9283                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9284                    }
9285                } else {
9286                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9287                    while matches!(self.peek(), Token::Comma) {
9288                        self.advance();
9289                        exprs.push(self.parse_expr(0)?);
9290                    }
9291                    if !matches!(self.peek(), Token::RParen) {
9292                        return Err(self.err(format!(
9293                            "expected ')' after SET row values, got {:?}",
9294                            self.peek()
9295                        )));
9296                    }
9297                    self.advance();
9298                    if exprs.len() != cols.len() {
9299                        return Err(self.err(alloc::format!(
9300                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9301                            cols.len(),
9302                            exprs.len()
9303                        )));
9304                    }
9305                    for (col, e) in cols.into_iter().zip(exprs) {
9306                        assignments.push((col, e));
9307                    }
9308                }
9309                if matches!(self.peek(), Token::Comma) {
9310                    self.advance();
9311                    continue;
9312                }
9313                break;
9314            }
9315            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9316            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9317            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9318            // `public.` dump qualifiers), so the qualifier has to be read off
9319            // the token stream first — otherwise `SET b.v = 888` would write
9320            // to the TARGET table's `v` while naming a source table, a
9321            // silent-wrong. A qualifier naming a SOURCE table means a
9322            // multi-TARGET update — mutating two tables in one statement —
9323            // which SPG does not model, so it is refused loudly.
9324            let set_qual: Option<String> = if mysql_from.is_some()
9325                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9326            {
9327                match self.peek() {
9328                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9329                    _ => None,
9330                }
9331            } else {
9332                None
9333            };
9334            let col = self.expect_ident_like()?;
9335            if let Some(q) = set_qual {
9336                let names_target = q.eq_ignore_ascii_case(&table)
9337                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9338                if !names_target {
9339                    return Err(self.err(alloc::format!(
9340                        "multi-table UPDATE can only assign to its first table \
9341                         ({table}); `{q}.{col}` targets another table"
9342                    )));
9343                }
9344            }
9345            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9346            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9347            // `__column_default` marker lowering just below). PG assigns to the
9348            // i-th (1-based) element, NULL-padding when i exceeds the length.
9349            if matches!(self.peek(), Token::LBracket) {
9350                self.advance();
9351                let index = self.parse_expr(0)?;
9352                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9353                // (and the open `arr[lo:]`), lowered to
9354                // `__array_assign_slice`. Only the single-subscript form
9355                // parsed before, so a slice assignment was a syntax error.
9356                let mut slice_hi: Option<Option<Expr>> = None;
9357                if matches!(self.peek(), Token::Colon) {
9358                    self.advance();
9359                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9360                        None
9361                    } else {
9362                        Some(self.parse_expr(0)?)
9363                    });
9364                }
9365                if !matches!(self.peek(), Token::RBracket) {
9366                    return Err(self.err(format!(
9367                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9368                        self.peek()
9369                    )));
9370                }
9371                self.advance();
9372                if !matches!(self.peek(), Token::Eq) {
9373                    return Err(self.err(format!(
9374                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9375                        self.peek()
9376                    )));
9377                }
9378                self.advance();
9379                let value = self.parse_expr(0)?;
9380                // PG merges several subscript writes to the same column into one
9381                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9382                // assignment to `col` rather than each overwriting the original.
9383                let existing = assignments.iter().position(|(c, _)| c == &col);
9384                let base = match existing {
9385                    Some(i) => assignments[i].1.clone(),
9386                    None => Expr::Column(ColumnName {
9387                        qualifier: None,
9388                        name: col.clone(),
9389                    }),
9390                };
9391                let assigned = match slice_hi {
9392                    None => Expr::FunctionCall {
9393                        name: "__array_assign".to_string(),
9394                        args: alloc::vec![base, index, value],
9395                    },
9396                    Some(hi) => Expr::FunctionCall {
9397                        name: "__array_assign_slice".to_string(),
9398                        args: alloc::vec![
9399                            base,
9400                            index,
9401                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9402                            value,
9403                        ],
9404                    },
9405                };
9406                match existing {
9407                    Some(i) => assignments[i].1 = assigned,
9408                    None => assignments.push((col, assigned)),
9409                }
9410                if matches!(self.peek(), Token::Comma) {
9411                    self.advance();
9412                    continue;
9413                }
9414                break;
9415            }
9416            if !matches!(self.peek(), Token::Eq) {
9417                return Err(self.err(format!(
9418                    "expected `=` after column name in UPDATE SET, got {:?}",
9419                    self.peek()
9420                )));
9421            }
9422            self.advance();
9423            // `SET col = DEFAULT` — the column's declared default;
9424            // rides out as a marker call the update executor
9425            // resolves against the schema.
9426            let value = if matches!(self.peek(), Token::Default) {
9427                self.advance();
9428                Expr::FunctionCall {
9429                    name: "__column_default".to_string(),
9430                    args: Vec::new(),
9431                }
9432            } else {
9433                self.parse_expr(0)?
9434            };
9435            assignments.push((col, value));
9436            if matches!(self.peek(), Token::Comma) {
9437                self.advance();
9438                continue;
9439            }
9440            break;
9441        }
9442        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9443        // update. Lowers onto the correlated-subquery machinery:
9444        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9445        // and each assignment that references a FROM-list table
9446        // wraps into a correlated scalar subquery
9447        // (SELECT expr FROM src WHERE cond). Equivalent for the
9448        // unique-join shape (the overwhelmingly common one); a
9449        // multi-match, which PG resolves by arbitrary pick,
9450        // surfaces as a scalar-subquery cardinality error instead
9451        // of a silent arbitrary result.
9452        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9453        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9454        // the SAME lowering below. Both spellings together is not legal in
9455        // either dialect.
9456        let from_clause = if let Some(fc) = mysql_from {
9457            if matches!(self.peek(), Token::From) {
9458                return Err(self.err(alloc::string::String::from(
9459                    "multi-table UPDATE already names its sources; drop the FROM clause",
9460                )));
9461            }
9462            Some(fc)
9463        } else if matches!(self.peek(), Token::From) {
9464            self.advance();
9465            Some(self.parse_from_clause()?)
9466        } else {
9467            None
9468        };
9469        let where_ = if matches!(self.peek(), Token::Where) {
9470            self.advance();
9471            Some(self.parse_expr(0)?)
9472        } else {
9473            None
9474        };
9475        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9476        // and the TARGET-row filter are NOT the same predicate once a LEFT
9477        // join is involved:
9478        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9479        //     one conjunction, and the whole thing filters target rows via
9480        //     EXISTS.
9481        //   * LEFT join: only the ON predicate belongs inside the source
9482        //     subquery. The WHERE still filters TARGET rows (with source
9483        //     columns read through the correlated subquery, which yields NULL
9484        //     for an unmatched row — exactly LEFT-join semantics).
9485        // Round 420 folded ON into WHERE unconditionally and then dropped the
9486        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9487        // WHERE a.id > 1` updated EVERY row.
9488        let sub_where = match (mysql_on.clone(), where_.clone()) {
9489            _ if mysql_outer => mysql_on.clone(),
9490            (Some(on), Some(w)) => Some(Expr::Binary {
9491                lhs: Box::new(on),
9492                op: crate::ast::BinOp::And,
9493                rhs: Box::new(w),
9494            }),
9495            (Some(on), None) => Some(on),
9496            (None, w) => w,
9497        };
9498        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9499        // has no such clause on UPDATE, so this is accepted only under the
9500        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9501        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9502        let mut returning = self.parse_optional_returning()?;
9503        // v7.39 (round 533) — kept for the engine, which can resolve the
9504        // UNQUALIFIED leaves this lowering has to leave alone.
9505        let from_sources = from_clause.as_ref().map(|fc| {
9506            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9507                from: fc.clone(),
9508                sub_where: sub_where.clone(),
9509            })
9510        });
9511        let (assignments, where_) = if let Some(fc) = from_clause {
9512            let names: Vec<String> = core::iter::once(&fc.primary)
9513                .chain(fc.joins.iter().map(|j| &j.table))
9514                .flat_map(|t| {
9515                    t.alias
9516                        .clone()
9517                        .into_iter()
9518                        .chain(core::iter::once(t.name.clone()))
9519                })
9520                .collect();
9521            let refs_list = |e: &Expr| -> bool {
9522                fn walk(e: &Expr, names: &[String]) -> bool {
9523                    match e {
9524                        Expr::Column(c) => c
9525                            .qualifier
9526                            .as_deref()
9527                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9528                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9529                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9530                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9531                        Expr::Case {
9532                            operand,
9533                            branches,
9534                            else_branch,
9535                        } => {
9536                            operand.as_deref().is_some_and(|o| walk(o, names))
9537                                || branches
9538                                    .iter()
9539                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9540                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9541                        }
9542                        _ => false,
9543                    }
9544                }
9545                walk(e, &names)
9546            };
9547            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9548                locking: None,
9549                ctes: Vec::new(),
9550                distinct: false,
9551                distinct_on: Vec::new(),
9552                items,
9553                from: Some(fc.clone()),
9554                where_: sub_where.clone(),
9555                group_by: None,
9556                group_by_all: false,
9557                having: None,
9558                unions: Vec::new(),
9559                order_by: Vec::new(),
9560                limit: None,
9561                offset: None,
9562                limit_with_ties: false,
9563                window_check_exprs: Vec::new(),
9564            };
9565            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9566            // assignment RHS with a correlated scalar subquery, instead of
9567            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9568            // column reference (`SET v = v + u.bonus`, where `v` is the target
9569            // table's column) inside a subquery whose FROM only has the source
9570            // table, so the unqualified `v` resolved against the source and
9571            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9572            // context — where they belong — fixes it; only the source columns
9573            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9574            // compound variants the leaf-walk doesn't decompose.
9575            let make_subq = |inner: Expr| {
9576                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9577                    expr: inner,
9578                    alias: None,
9579                }])))
9580            };
9581            let assignments = assignments
9582                .into_iter()
9583                .map(|(col, mut expr)| {
9584                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9585                    (col, expr)
9586                })
9587                .collect();
9588            let exists = Expr::Exists {
9589                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9590                    expr: Expr::Literal(Literal::Integer(1)),
9591                    alias: None,
9592                }])),
9593                negated: false,
9594            };
9595            // v7.39 (round 241) — RETURNING may reference the FROM-list
9596            // tables too (`RETURNING emp.id, dept.name`); the same
9597            // leaf-to-correlated-subquery lowering the assignments get.
9598            // Without it the qualifier died at eval with "unknown table
9599            // qualifier". (RETURNING was parsed before this block — the
9600            // lowering is a pure AST transformation.)
9601            if let Some(items) = returning.as_mut() {
9602                for item in items.iter_mut() {
9603                    if let SelectItem::Expr { expr, .. } = item {
9604                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9605                    }
9606                }
9607            }
9608            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9609            // EVERY matching target row: it gets no EXISTS filter, but the
9610            // caller's WHERE still applies, with source columns read through
9611            // the correlated subquery (NULL when unmatched — LEFT-join
9612            // semantics). `sub_where` above already excluded the WHERE from
9613            // the source subquery for this case.
9614            if mysql_outer {
9615                let mut outer = where_;
9616                if let Some(w) = outer.as_mut() {
9617                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9618                }
9619                (assignments, outer)
9620            } else {
9621                (assignments, Some(exists))
9622            }
9623        } else {
9624            (assignments, where_)
9625        };
9626        Ok(Statement::Update(crate::ast::UpdateStatement {
9627            ctes: Vec::new(),
9628            table,
9629            only,
9630            alias,
9631            assignments,
9632            from_sources,
9633            where_,
9634            order_limit: update_order_limit,
9635            returning,
9636        }))
9637    }
9638
9639    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9640    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9641    /// clause and its meaning are identical, so both call this rather than
9642    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9643    /// legal. PG has no such clause on either statement, so it is read only
9644    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9645    /// errors.
9646    ///
9647    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9648    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9649    /// stack in round 430.
9650    #[inline(never)]
9651    fn parse_mysql_dml_order_limit(
9652        &mut self,
9653        what: &str,
9654    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9655        if !self.mysql_dialect {
9656            return Ok(None);
9657        }
9658        let order_by = self.parse_order_by_keys()?;
9659        let limit = if matches!(self.peek(), Token::Limit) {
9660            self.advance();
9661            let tok = self.advance();
9662            let Token::Integer(n) = tok else {
9663                return Err(self.err(alloc::format!(
9664                    "expected integer after {what} LIMIT, got {tok:?}"
9665                )));
9666            };
9667            // MySQL rejects the `LIMIT offset, count` form here — only a
9668            // single row count is legal on a DML statement.
9669            if matches!(self.peek(), Token::Comma) {
9670                return Err(self.err(alloc::format!(
9671                    "{what} LIMIT takes a row count, not an offset"
9672                )));
9673            }
9674            let n = u32::try_from(n)
9675                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9676            Some(n)
9677        } else {
9678            None
9679        };
9680        if order_by.is_empty() && limit.is_none() {
9681            return Ok(None);
9682        }
9683        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9684            order_by,
9685            limit,
9686        })))
9687    }
9688
9689    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9690    /// the leading `DELETE` ident.
9691    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9692        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9693        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9694        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9695        // parse here; it reaches the existing USING path with the target
9696        // repeated in the list, which the source-list peel below handles.)
9697        // More than one name is a multi-TARGET delete, which SPG does not
9698        // model; it is refused rather than half-applied.
9699        let mysql_pre_target: Option<String> =
9700            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9701                let first = self.expect_ident_like()?;
9702                if matches!(self.peek(), Token::Comma) {
9703                    return Err(self.err(alloc::format!(
9704                        "multi-table DELETE can only delete from one table; \
9705                     `DELETE {first}, …` names several"
9706                    )));
9707                }
9708                Some(first)
9709            } else {
9710                None
9711            };
9712        if !matches!(self.peek(), Token::From) {
9713            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9714        }
9715        self.advance();
9716        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9717        // lookahead as the UPDATE spelling.
9718        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9719            if s.eq_ignore_ascii_case("only"))
9720            && matches!(
9721                self.tokens.get(self.pos + 1),
9722                Some(Token::Ident(_) | Token::QuotedIdent(_))
9723            );
9724        if only {
9725            self.advance();
9726        }
9727        let table = self.expect_ident_like()?;
9728        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9729        // spelling must not swallow the clause keywords that can follow
9730        // the target.
9731        let alias = if matches!(self.peek(), Token::As) {
9732            self.advance();
9733            Some(self.expect_ident_like()?)
9734        } else {
9735            match self.peek() {
9736                Token::Ident(s) | Token::QuotedIdent(s)
9737                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9738                {
9739                    let a = s.clone();
9740                    self.advance();
9741                    Some(a)
9742                }
9743                _ => None,
9744            }
9745        };
9746        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9747        // through the SAME join grammar the FROM clause uses (see the
9748        // `advance()`-destroys-tokens note on `parse_from_joins`).
9749        let mut mysql_on: Option<Expr> = None;
9750        let mut mysql_outer = false;
9751        let mysql_using = if mysql_pre_target.is_some()
9752            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9753        {
9754            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9755            let mut joins = self.parse_from_joins(&target_qual)?;
9756            if joins.is_empty() {
9757                return Err(self.err(alloc::string::String::from(
9758                    "multi-table DELETE needs at least one source table",
9759                )));
9760            }
9761            let head = joins.remove(0);
9762            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9763            mysql_on = head.on;
9764            Some(FromClause {
9765                primary: head.table,
9766                joins,
9767            })
9768        } else {
9769            None
9770        };
9771        // The pre-FROM target must be the table the FROM names (or its
9772        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9773        // is not the scan target.
9774        if let Some(t) = &mysql_pre_target {
9775            let names_target = t.eq_ignore_ascii_case(&table)
9776                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9777            if !names_target {
9778                return Err(self.err(alloc::format!(
9779                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9780                )));
9781            }
9782        }
9783        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9784        // delete. Same lowering as UPDATE … FROM: the WHERE
9785        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9786        // target row by the correlated machinery.
9787        let using_clause = if let Some(fc) = mysql_using {
9788            Some(fc)
9789        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9790            self.advance();
9791            let mut fc = self.parse_from_clause()?;
9792            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9793            // repeats the TARGET as the first USING entry (PG's spelling
9794            // lists only the extra sources). Peel it so the source subquery
9795            // does not re-scan — and shadow — the target table.
9796            let primary_is_target =
9797                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9798            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9799                let head = fc.joins.remove(0);
9800                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9801                mysql_on = head.on;
9802                fc = FromClause {
9803                    primary: head.table,
9804                    joins: fc.joins,
9805                };
9806            }
9807            Some(fc)
9808        } else {
9809            None
9810        };
9811        let where_ = if matches!(self.peek(), Token::Where) {
9812            self.advance();
9813            Some(self.parse_expr(0)?)
9814        } else {
9815            None
9816        };
9817        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9818        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9819        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9820        let mut returning = self.parse_optional_returning()?;
9821        let where_ = if let Some(fc) = using_clause {
9822            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9823            // a USING-table reference in RETURNING becomes a correlated
9824            // scalar subquery over the USING list.
9825            let names: Vec<String> = core::iter::once(&fc.primary)
9826                .chain(fc.joins.iter().map(|j| &j.table))
9827                .flat_map(|t| {
9828                    t.alias
9829                        .clone()
9830                        .into_iter()
9831                        .chain(core::iter::once(t.name.clone()))
9832                })
9833                .collect();
9834            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9835            // join filters the SOURCE subquery on the ON predicate alone and
9836            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9837            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9838            // rows); every other form folds ON and WHERE into one EXISTS.
9839            let sub_where = match (mysql_on.clone(), where_.clone()) {
9840                _ if mysql_outer => mysql_on.clone(),
9841                (Some(on), Some(w)) => Some(Expr::Binary {
9842                    lhs: Box::new(on),
9843                    op: crate::ast::BinOp::And,
9844                    rhs: Box::new(w),
9845                }),
9846                (Some(on), None) => Some(on),
9847                (None, w) => w,
9848            };
9849            let exists_where = sub_where.clone();
9850            let sub_fc = fc.clone();
9851            let make_subq = move |leaf: Expr| -> Expr {
9852                Expr::ScalarSubquery(Box::new(SelectStatement {
9853                    locking: None,
9854                    ctes: Vec::new(),
9855                    distinct: false,
9856                    distinct_on: Vec::new(),
9857                    items: alloc::vec![SelectItem::Expr {
9858                        expr: leaf,
9859                        alias: None,
9860                    }],
9861                    from: Some(sub_fc.clone()),
9862                    where_: sub_where.clone(),
9863                    group_by: None,
9864                    group_by_all: false,
9865                    having: None,
9866                    unions: Vec::new(),
9867                    order_by: Vec::new(),
9868                    limit: None,
9869                    offset: None,
9870                    limit_with_ties: false,
9871                    window_check_exprs: Vec::new(),
9872                }))
9873            };
9874            let refs = |e: &Expr| expr_refs_tables(e, &names);
9875            if let Some(items) = returning.as_mut() {
9876                for item in items.iter_mut() {
9877                    if let SelectItem::Expr { expr, .. } = item {
9878                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9879                    }
9880                }
9881            }
9882            // A LEFT join deletes the target rows the WHERE selects, reading
9883            // source columns through the correlated subquery (NULL when
9884            // unmatched); no EXISTS row filter.
9885            if mysql_outer {
9886                let mut outer = where_;
9887                if let Some(w) = outer.as_mut() {
9888                    wrap_from_leaves(w, &names, &make_subq, &refs);
9889                }
9890                outer
9891            } else {
9892                Some(Expr::Exists {
9893                    subquery: Box::new(SelectStatement {
9894                        locking: None,
9895                        ctes: Vec::new(),
9896                        distinct: false,
9897                        distinct_on: Vec::new(),
9898                        items: alloc::vec![SelectItem::Expr {
9899                            expr: Expr::Literal(Literal::Integer(1)),
9900                            alias: None,
9901                        }],
9902                        from: Some(fc),
9903                        where_: exists_where,
9904                        group_by: None,
9905                        group_by_all: false,
9906                        having: None,
9907                        unions: Vec::new(),
9908                        order_by: Vec::new(),
9909                        limit: None,
9910                        offset: None,
9911                        limit_with_ties: false,
9912                        window_check_exprs: Vec::new(),
9913                    }),
9914                    negated: false,
9915                })
9916            }
9917        } else {
9918            where_
9919        };
9920        Ok(Statement::Delete(crate::ast::DeleteStatement {
9921            ctes: Vec::new(),
9922            table,
9923            only,
9924            alias,
9925            where_,
9926            order_limit: delete_order_limit,
9927            returning,
9928        }))
9929    }
9930
9931    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9932    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9933    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9934    /// keyword. v7.17 surface:
9935    ///   * source: table reference (subquery source is a follow-up)
9936    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9937    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9938    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
9939    ///     order
9940    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9941        // INTO
9942        let is_into_kw = matches!(self.peek(), Token::Into)
9943            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9944        if !is_into_kw {
9945            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9946        }
9947        self.advance();
9948        let target = self.expect_ident_like()?;
9949        // Optional alias — bare ident before USING.
9950        let target_alias = match self.peek() {
9951            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9952                Some(self.expect_ident_like()?)
9953            }
9954            _ => None,
9955        };
9956        // USING
9957        let is_using_kw = matches!(
9958            self.peek(),
9959            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9960        );
9961        if !is_using_kw {
9962            return Err(self.err(format!(
9963                "expected USING after MERGE INTO target, got {:?}",
9964                self.peek()
9965            )));
9966        }
9967        self.advance();
9968        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9969        // <table> [alias]`. PG requires an alias after a subquery source.
9970        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9971            self.advance(); // (
9972            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9973            // constant-SELECT lowering the derived-table parser uses
9974            // (PG deletes through this form; it was a parse error).
9975            let inner = if matches!(self.peek(), Token::Values) {
9976                self.advance(); // VALUES
9977                Statement::Select(self.parse_values_rows_body()?)
9978            } else {
9979                self.parse_select_stmt()?
9980            };
9981            match self.advance() {
9982                Token::RParen => {}
9983                other => {
9984                    return Err(self.err(format!(
9985                        "expected ')' after MERGE USING subquery, got {other:?}"
9986                    )));
9987                }
9988            }
9989            let Statement::Select(sub) = inner else {
9990                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9991            };
9992            (String::new(), Some(Box::new(sub)))
9993        } else {
9994            (self.expect_ident_like()?, None)
9995        };
9996        let source_alias = match self.peek() {
9997            Token::Ident(s) | Token::QuotedIdent(s)
9998                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9999            {
10000                Some(self.expect_ident_like()?)
10001            }
10002            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10003                self.advance(); // AS
10004                Some(self.expect_ident_like()?)
10005            }
10006            _ => None,
10007        };
10008        // v7.39 (round 768, F31-D5) — optional positional column-alias
10009        // list after the source alias (`s(id, v)`).
10010        let mut source_column_aliases: Vec<String> = Vec::new();
10011        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10012            self.advance();
10013            loop {
10014                source_column_aliases.push(self.expect_ident_like()?);
10015                match self.peek() {
10016                    Token::Comma => {
10017                        self.advance();
10018                    }
10019                    Token::RParen => {
10020                        self.advance();
10021                        break;
10022                    }
10023                    other => {
10024                        return Err(self.err(format!(
10025                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10026                        )));
10027                    }
10028                }
10029            }
10030        }
10031        if source_select.is_some() && source_alias.is_none() {
10032            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10033        }
10034        // ON
10035        if !matches!(self.peek(), Token::On) {
10036            return Err(self.err(format!(
10037                "expected ON after MERGE … USING source, got {:?}",
10038                self.peek()
10039            )));
10040        }
10041        self.advance();
10042        let on = self.parse_expr(0)?;
10043        // One or more WHEN clauses.
10044        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10045        loop {
10046            let is_when_kw = matches!(
10047                self.peek(),
10048                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10049            );
10050            if !is_when_kw {
10051                break;
10052            }
10053            self.advance(); // WHEN
10054            // [NOT] MATCHED
10055            let matched = if matches!(self.peek(), Token::Not) {
10056                self.advance();
10057                crate::ast::MergeMatched::NotMatched
10058            } else {
10059                crate::ast::MergeMatched::Matched
10060            };
10061            let is_matched_kw = matches!(
10062                self.peek(),
10063                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10064            );
10065            if !is_matched_kw {
10066                return Err(self.err(format!(
10067                    "expected MATCHED in WHEN clause, got {:?}",
10068                    self.peek()
10069                )));
10070            }
10071            self.advance();
10072            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10073            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10074            // to fire for target rows no source row matches.
10075            let mut matched = matched;
10076            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10077                self.advance();
10078                match self.peek() {
10079                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10080                        self.advance();
10081                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10082                    }
10083                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10084                        self.advance();
10085                    }
10086                    other => {
10087                        return Err(self.err(format!(
10088                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10089                        )));
10090                    }
10091                }
10092            }
10093            // Optional AND <expr>
10094            let condition = if matches!(self.peek(), Token::And) {
10095                self.advance();
10096                Some(self.parse_expr(0)?)
10097            } else {
10098                None
10099            };
10100            // THEN
10101            let is_then_kw = matches!(
10102                self.peek(),
10103                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10104            );
10105            if !is_then_kw {
10106                return Err(self.err(format!(
10107                    "expected THEN in WHEN clause, got {:?}",
10108                    self.peek()
10109                )));
10110            }
10111            self.advance();
10112            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10113            let action = match self.peek().clone() {
10114                Token::Insert => {
10115                    self.advance();
10116                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10117                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10118                    // VALUES (…)` omits it and fills every column in declaration
10119                    // order. PG accepts this; SPG used to require the list.
10120                    let mut columns: Vec<String> = Vec::new();
10121                    if matches!(self.peek(), Token::LParen) {
10122                        self.advance();
10123                        loop {
10124                            columns.push(self.expect_ident_like()?);
10125                            if matches!(self.peek(), Token::Comma) {
10126                                self.advance();
10127                                continue;
10128                            }
10129                            break;
10130                        }
10131                        if !matches!(self.peek(), Token::RParen) {
10132                            return Err(self.err(format!(
10133                                "expected ')' after INSERT column list, got {:?}",
10134                                self.peek()
10135                            )));
10136                        }
10137                        self.advance();
10138                    }
10139                    // VALUES (...)
10140                    if !matches!(self.peek(), Token::Values) {
10141                        return Err(self.err(format!(
10142                            "expected VALUES in MERGE INSERT, got {:?}",
10143                            self.peek()
10144                        )));
10145                    }
10146                    self.advance();
10147                    if !matches!(self.peek(), Token::LParen) {
10148                        return Err(self.err(format!(
10149                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10150                            self.peek()
10151                        )));
10152                    }
10153                    self.advance();
10154                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10155                    loop {
10156                        values.push(self.parse_expr(0)?);
10157                        if matches!(self.peek(), Token::Comma) {
10158                            self.advance();
10159                            continue;
10160                        }
10161                        break;
10162                    }
10163                    if !matches!(self.peek(), Token::RParen) {
10164                        return Err(self.err(format!(
10165                            "expected ')' after MERGE INSERT values, got {:?}",
10166                            self.peek()
10167                        )));
10168                    }
10169                    self.advance();
10170                    // Empty column list = positional into every column, so the
10171                    // count is checked against the table arity at execution.
10172                    if !columns.is_empty() && columns.len() != values.len() {
10173                        return Err(self.err(format!(
10174                            "MERGE INSERT column count ({}) ≠ value count ({})",
10175                            columns.len(),
10176                            values.len()
10177                        )));
10178                    }
10179                    crate::ast::MergeAction::Insert { columns, values }
10180                }
10181                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10182                    self.advance();
10183                    // SET
10184                    let is_set_kw = matches!(
10185                        self.peek(),
10186                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10187                    );
10188                    if !is_set_kw {
10189                        return Err(self.err(format!(
10190                            "expected SET after UPDATE in MERGE, got {:?}",
10191                            self.peek()
10192                        )));
10193                    }
10194                    self.advance();
10195                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10196                    loop {
10197                        let col = self.expect_ident_like()?;
10198                        if !matches!(self.peek(), Token::Eq) {
10199                            return Err(self.err(format!(
10200                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10201                                self.peek()
10202                            )));
10203                        }
10204                        self.advance();
10205                        let expr = self.parse_expr(0)?;
10206                        assignments.push((col, expr));
10207                        if matches!(self.peek(), Token::Comma) {
10208                            self.advance();
10209                            continue;
10210                        }
10211                        break;
10212                    }
10213                    crate::ast::MergeAction::Update { assignments }
10214                }
10215                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10216                    self.advance();
10217                    crate::ast::MergeAction::Delete
10218                }
10219                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10220                    self.advance();
10221                    let is_nothing_kw = matches!(
10222                        self.peek(),
10223                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10224                    );
10225                    if !is_nothing_kw {
10226                        return Err(self.err(format!(
10227                            "expected NOTHING after DO in MERGE clause, got {:?}",
10228                            self.peek()
10229                        )));
10230                    }
10231                    self.advance();
10232                    crate::ast::MergeAction::DoNothing
10233                }
10234                other => {
10235                    return Err(self.err(format!(
10236                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10237                    )));
10238                }
10239            };
10240            // PG's grammar simply has no INSERT production under BY SOURCE
10241            // (a target row already exists there) — same syntax error.
10242            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10243                && matches!(action, crate::ast::MergeAction::Insert { .. })
10244            {
10245                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10246            }
10247            clauses.push(crate::ast::MergeWhenClause {
10248                matched,
10249                condition,
10250                action,
10251            });
10252        }
10253        if clauses.is_empty() {
10254            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10255        }
10256        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10257        // unconditional (no `AND`) WHEN of the same match kind: it could
10258        // never fire. Check per match kind in clause order.
10259        let mut seen_unconditional_matched = false;
10260        let mut seen_unconditional_not_matched = false;
10261        let mut seen_unconditional_by_source = false;
10262        for c in &clauses {
10263            let seen = match c.matched {
10264                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10265                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10266                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10267            };
10268            if *seen {
10269                return Err(self.err(String::from(
10270                    "unreachable WHEN clause specified after unconditional WHEN clause",
10271                )));
10272            }
10273            if c.condition.is_none() {
10274                *seen = true;
10275            }
10276        }
10277        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10278        let returning = self.parse_optional_returning()?;
10279        Ok(Statement::Merge(crate::ast::MergeStatement {
10280            // Attached by `parse_with_cte_then_select` when the MERGE
10281            // heads a WITH clause (round 149).
10282            ctes: Vec::new(),
10283            target,
10284            target_alias,
10285            source,
10286            source_alias,
10287            source_select,
10288            source_column_aliases,
10289            on,
10290            clauses,
10291            returning,
10292        }))
10293    }
10294
10295    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10296    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10297    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10298    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10299    fn parse_optional_returning(
10300        &mut self,
10301    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10302        let is_returning_kw = matches!(
10303            self.peek(),
10304            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10305        );
10306        if !is_returning_kw {
10307            return Ok(None);
10308        }
10309        self.advance();
10310        let mut items = Vec::new();
10311        loop {
10312            items.push(self.parse_select_item()?);
10313            if matches!(self.peek(), Token::Comma) {
10314                self.advance();
10315                continue;
10316            }
10317            break;
10318        }
10319        Ok(Some(items))
10320    }
10321
10322    /// v6.0.4 — parse the tail of an ALTER statement after the
10323    /// leading `ALTER` keyword has been consumed. Only one form is
10324    /// supported in v6.0.4:
10325    ///
10326    /// ```text
10327    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10328    /// ```
10329    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10330        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10331        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10332        // exclusion) is accepted by stripping the `ONLY` keyword
10333        // before the table parse.
10334        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10335        // and the long PG-dump tail are accepted as no-ops.
10336        match self.advance() {
10337            Token::Index => {}
10338            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10339            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10340            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10341            Token::Table => {
10342                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10343                    self.advance();
10344                }
10345                return self.parse_alter_table_after_keyword();
10346            }
10347            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10348                return self.parse_alter_policy_after_keyword();
10349            }
10350            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10351                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10352                    self.advance();
10353                }
10354                return self.parse_alter_table_after_keyword();
10355            }
10356            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10357            // of the silent-noop tail.
10358            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10359                return self.parse_alter_sequence_after_keyword();
10360            }
10361            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10362            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10363            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10364            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10365                // NB: the match arm consumed `TYPE` via self.advance(); the
10366                // cursor is now at the type name — do NOT advance again.
10367                let type_name = self.expect_ident_like()?;
10368                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10369                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10370                if is_add_value {
10371                    self.advance(); // ADD
10372                    self.advance(); // VALUE
10373                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10374                    // IF/EXISTS as identifiers.
10375                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10376                    {
10377                        let n1 = self.tokens.get(self.pos + 1);
10378                        let n2 = self.tokens.get(self.pos + 2);
10379                        if matches!(n1, Some(Token::Not))
10380                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10381                        {
10382                            self.advance();
10383                            self.advance();
10384                            self.advance();
10385                            true
10386                        } else {
10387                            false
10388                        }
10389                    } else {
10390                        false
10391                    };
10392                    let label = self.expect_string_literal()?;
10393                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10394                    {
10395                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10396                        self.advance();
10397                        let anchor = self.expect_string_literal()?;
10398                        Some((is_before, anchor))
10399                    } else {
10400                        None
10401                    };
10402                    return Ok(Statement::AlterTypeAddValue {
10403                        type_name,
10404                        label,
10405                        if_not_exists,
10406                        position,
10407                    });
10408                }
10409                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10410                // Used to fall into the no-op tail below: accepted, silently
10411                // ignored. `RENAME TO <newtype>` keeps falling through.
10412                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10413                    && matches!(
10414                        self.tokens.get(self.pos + 1),
10415                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10416                    )
10417                {
10418                    self.advance(); // RENAME
10419                    self.advance(); // VALUE
10420                    let old = self.expect_string_literal()?;
10421                    if matches!(self.peek(), Token::To) {
10422                        self.advance();
10423                    } else {
10424                        self.expect_keyword_ident("to")?;
10425                    }
10426                    let new = self.expect_string_literal()?;
10427                    return Ok(Statement::AlterTypeRenameValue {
10428                        type_name,
10429                        old,
10430                        new,
10431                    });
10432                }
10433                // Other ALTER TYPE forms — the ACTION stays a no-op
10434                // (pg_dump tail), but v7.39 (round 708) the NAME is
10435                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10436                // success for a type that does not exist.
10437                self.consume_until_statement_boundary();
10438                return Ok(Statement::ValidateOnly {
10439                    kind: crate::ast::ValidateOnlyKind::TypeName,
10440                    names: alloc::vec![type_name],
10441                });
10442            }
10443            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10444            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10445            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10446            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10447            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10448            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10449            // pg_dump no-op list below: every form used to report success
10450            // and change nothing, which is worse than refusing outright
10451            // (a migration dropping a constraint kept rejecting data).
10452            // NOTE: the enclosing `match self.advance()` already consumed
10453            // the DOMAIN keyword, so the name is next.
10454            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10455                return self.parse_alter_domain_after_keyword();
10456            }
10457            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10458            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10459            // used to fall into the pg_dump no-op tail below, so a DBA
10460            // setting a per-role default was told it worked and nothing
10461            // happened. Intercepted here, BEFORE that tail.
10462            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10463            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10464            // interception below exists: swallowed with the no-op tail, an
10465            // unknown parameter name was ACCEPTED where PG18 answers
10466            // `unrecognized configuration parameter`. SPG applies nothing
10467            // either way — there is no postgresql.auto.conf — but it now
10468            // says so about a name it does not know.
10469            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10470                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10471                // already consumed here. An extra advance eats the SET and
10472                // the parameter name is never seen — which is exactly the
10473                // bug a panic in this branch disproved: the branch WAS on
10474                // the path, the reading of it was wrong.
10475                let mut parameter = None;
10476                // SET <name> … | RESET <name> | RESET ALL
10477                if matches!(self.peek(), Token::Ident(k)
10478                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10479                {
10480                    self.advance();
10481                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10482                        && !n.eq_ignore_ascii_case("all")
10483                    {
10484                        self.advance();
10485                        // A dotted GUC (`plpgsql.check_asserts`) is two
10486                        // tokens; keep the whole name.
10487                        let mut full = n;
10488                        while matches!(self.peek(), Token::Dot) {
10489                            self.advance();
10490                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10491                                full.push('.');
10492                                full.push_str(&t);
10493                            }
10494                        }
10495                        parameter = Some(full);
10496                    }
10497                }
10498                self.consume_until_statement_boundary();
10499                return Ok(Statement::AlterSystem { parameter });
10500            }
10501            Token::Ident(s) | Token::QuotedIdent(s)
10502                if matches!(
10503                    s.to_ascii_lowercase().as_str(),
10504                    "role" | "user" | "database"
10505                ) && self.peeks_db_role_setting() =>
10506            {
10507                let is_database = s.eq_ignore_ascii_case("database");
10508                return self.parse_db_role_setting(is_database);
10509            }
10510            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10511            // (the non-SET forms; SET/RESET took the branch above). The
10512            // attributes still no-op — recorded, and the ignored PASSWORD
10513            // is ledgered as its own follow-up — but the ROLE is validated:
10514            // any name was accepted for a role that does not exist.
10515            Token::Ident(s) | Token::QuotedIdent(s)
10516                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10517            {
10518                // NB: the enclosing `match self.advance()` already consumed
10519                // ROLE/USER — the round-695 trap, hit again in this round's
10520                // first draft (the name was eaten and WITH parsed as the
10521                // role). The cursor is at the name.
10522                let name = self.expect_ident_or_string()?;
10523                // v7.39 (round 750) — scan the attribute tail for
10524                // PASSWORD. Everything else stays a recorded no-op, but
10525                // a dropped credential rotation is a SECURITY bug:
10526                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10527                // changed nothing, so the old password kept working.
10528                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10529                // NULL` clears the credential.
10530                let mut password: Option<Option<String>> = None;
10531                loop {
10532                    match self.peek() {
10533                        Token::Semicolon | Token::Eof => break,
10534                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10535                            self.advance();
10536                            match self.advance() {
10537                                Token::String(p) => password = Some(Some(p)),
10538                                Token::Null => password = Some(None),
10539                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10540                                    password = Some(None);
10541                                }
10542                                other => {
10543                                    return Err(self.err(alloc::format!(
10544                                        "expected password string or NULL after PASSWORD, got {other:?}"
10545                                    )));
10546                                }
10547                            }
10548                        }
10549                        _ => {
10550                            self.advance();
10551                        }
10552                    }
10553                }
10554                if name.eq_ignore_ascii_case("all") {
10555                    // `ALTER ROLE ALL …` names every role; nothing to check.
10556                    return Ok(Statement::Empty);
10557                }
10558                if let Some(pw) = password {
10559                    return Ok(Statement::AlterRolePassword { name, password: pw });
10560                }
10561                return Ok(Statement::ValidateOnly {
10562                    kind: crate::ast::ValidateOnlyKind::RoleName,
10563                    names: alloc::vec![name],
10564                });
10565            }
10566            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10567            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10568            // list far enough to validate the NAME; the actions still no-op.
10569            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10570            // models none of them and their dumps are rare.)
10571            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10572                let name = self.expect_ident_or_string()?;
10573                self.consume_until_statement_boundary();
10574                return Ok(Statement::ValidateOnly {
10575                    kind: crate::ast::ValidateOnlyKind::CollationName,
10576                    names: alloc::vec![name],
10577                });
10578            }
10579            Token::Ident(s) | Token::QuotedIdent(s)
10580                if s.eq_ignore_ascii_case("text")
10581                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10582                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10583            {
10584                self.advance(); // SEARCH
10585                self.advance(); // CONFIGURATION
10586                let name = self.expect_ident_like()?;
10587                self.consume_until_statement_boundary();
10588                return Ok(Statement::ValidateOnly {
10589                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10590                    names: alloc::vec![name],
10591                });
10592            }
10593            Token::Ident(s) | Token::QuotedIdent(s)
10594                if s.eq_ignore_ascii_case("event")
10595                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10596            {
10597                self.advance(); // TRIGGER
10598                let name = self.expect_ident_like()?;
10599                self.consume_until_statement_boundary();
10600                return Ok(Statement::ValidateOnly {
10601                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10602                    names: alloc::vec![name],
10603                });
10604            }
10605            Token::Ident(s) | Token::QuotedIdent(s)
10606                if s.eq_ignore_ascii_case("large")
10607                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10608            {
10609                self.advance(); // OBJECT
10610                let oid = match self.advance() {
10611                    Token::Integer(n) => alloc::format!("{n}"),
10612                    other => {
10613                        return Err(
10614                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10615                        );
10616                    }
10617                };
10618                self.consume_until_statement_boundary();
10619                return Ok(Statement::ValidateOnly {
10620                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10621                    names: alloc::vec![oid],
10622                });
10623            }
10624            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10625            // argument-list parse as DROP AGGREGATE (round 707); the
10626            // action no-ops, the existence check is real.
10627            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10628                // Same round-695 trap as above: AGGREGATE is already
10629                // consumed; the cursor is at the name.
10630                let name = self.expect_ident_like()?;
10631                let mut names = alloc::vec![name];
10632                if matches!(self.peek(), Token::LParen) {
10633                    self.advance();
10634                    loop {
10635                        match self.peek().clone() {
10636                            Token::RParen => {
10637                                self.advance();
10638                                break;
10639                            }
10640                            Token::Star => {
10641                                self.advance();
10642                                names.push(String::from("*"));
10643                            }
10644                            Token::Comma => {
10645                                self.advance();
10646                            }
10647                            _ => {
10648                                let mut t = self.expect_ident_like()?;
10649                                while let Token::Ident(nx) = self.peek() {
10650                                    let nx = nx.clone();
10651                                    self.advance();
10652                                    t.push(' ');
10653                                    t.push_str(&nx);
10654                                }
10655                                names.push(t);
10656                            }
10657                        }
10658                    }
10659                }
10660                self.consume_until_statement_boundary();
10661                return Ok(Statement::ValidateOnly {
10662                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10663                    names,
10664                });
10665            }
10666            Token::Ident(s) | Token::QuotedIdent(s)
10667                if matches!(
10668                    s.to_ascii_lowercase().as_str(),
10669                    "view"
10670                        | "function"
10671                        | "database"
10672                        | "schema"
10673                        | "owner"
10674                        | "default"
10675                        | "extension"
10676                        | "materialized"
10677                        | "publication"
10678                        | "subscription"
10679                        // v7.37.17 (17.6 siblings) — additional ALTER
10680                        // targets pg_dump / pg_dumpall / operator DB
10681                        // migration scripts commonly emit. SPG has
10682                        // no matching machinery for any of these; the
10683                        // parser accepts + Empty-returns so pg_dump
10684                        // tail statements don't stall.
10685                        | "tablespace"
10686                        | "language"
10687                        | "operator"
10688                        | "conversion"
10689                        | "statistics"
10690                        | "server"
10691                        | "foreign"
10692                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10693                        // / TEMPLATE (CONFIGURATION intercepted above).
10694                        | "text"
10695                ) =>
10696            {
10697                self.consume_until_statement_boundary();
10698                return Ok(Statement::Empty);
10699            }
10700            other => {
10701                return Err(self.err(format!(
10702                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10703                     after ALTER, got {other:?}"
10704                )));
10705            }
10706        }
10707        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10708        // (mailrs migrate-042 ships these). The presence of an
10709        // IF EXISTS makes the subsequent name lookup tolerate
10710        // a missing index — engine returns CommandOk no-op.
10711        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10712            let next = self.tokens.get(self.pos + 1);
10713            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10714                self.advance();
10715                self.advance();
10716                true
10717            } else {
10718                false
10719            }
10720        } else {
10721            false
10722        };
10723        let name = self.expect_ident_like()?;
10724        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10725        // Detect BEFORE the REBUILD path so the existing REBUILD
10726        // arm stays untouched.
10727        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10728            self.advance();
10729            if matches!(self.peek(), Token::To) {
10730                self.advance();
10731            } else {
10732                self.expect_keyword_ident("to")?;
10733            }
10734            let new = self.expect_ident_like()?;
10735            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10736                name,
10737                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10738            }));
10739        }
10740        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10741        // A syntax error before; the index is validated, the params no-op.
10742        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10743            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10744                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10745        {
10746            self.consume_until_statement_boundary();
10747            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10748                name,
10749                target: crate::ast::AlterIndexTarget::StorageParams,
10750            }));
10751        }
10752        // REBUILD
10753        self.expect_keyword_ident("rebuild")?;
10754        // Optional: WITH (encoding = <enc>)
10755        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10756            self.advance();
10757            if !matches!(self.peek(), Token::LParen) {
10758                return Err(self.err(format!(
10759                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10760                    self.peek()
10761                )));
10762            }
10763            self.advance();
10764            self.expect_keyword_ident("encoding")?;
10765            if !matches!(self.peek(), Token::Eq) {
10766                return Err(self.err(format!(
10767                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10768                    self.peek()
10769                )));
10770            }
10771            self.advance();
10772            let enc_ident = match self.advance() {
10773                Token::Ident(s) | Token::QuotedIdent(s) => s,
10774                other => {
10775                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10776                }
10777            };
10778            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10779                "f32" => VecEncoding::F32,
10780                "sq8" => VecEncoding::Sq8,
10781                "half" => VecEncoding::F16,
10782                other => {
10783                    return Err(self.err(format!(
10784                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10785                    )));
10786                }
10787            };
10788            if !matches!(self.peek(), Token::RParen) {
10789                return Err(self.err(format!(
10790                    "expected ')' after encoding value, got {:?}",
10791                    self.peek()
10792                )));
10793            }
10794            self.advance();
10795            Some(enc)
10796        } else {
10797            None
10798        };
10799        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10800            name,
10801            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10802        }))
10803    }
10804
10805    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10806    /// only `SET` form currently supported; future v6.7.x can add
10807    /// more SET subjects without changing the dispatch shape.
10808    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10809    /// subactions. Single-subaction shape stays a 1-element vec.
10810    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10811        let table_name = self.expect_ident_like()?;
10812        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10813        loop {
10814            let subaction = self.parse_alter_table_subaction()?;
10815            // ADD COLUMN with inline REFERENCES emits both an
10816            // AddColumn and an AddForeignKey subaction; the
10817            // helper returns 1 or 2 items.
10818            targets.extend(subaction);
10819            if matches!(self.peek(), Token::Comma) {
10820                self.advance();
10821                continue;
10822            }
10823            break;
10824        }
10825        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10826            name: table_name,
10827            targets,
10828        }))
10829    }
10830
10831    /// Parse one ALTER TABLE subaction. Returns a Vec because
10832    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10833    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10834    fn parse_alter_table_subaction(
10835        &mut self,
10836    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10837        match self.peek() {
10838            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10839                self.advance();
10840                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10841                // storage parameters: paren-prefixed; consume.
10842                if matches!(self.peek(), Token::LParen) {
10843                    self.consume_until_statement_boundary();
10844                    return Ok(Vec::new());
10845                }
10846                let setting = self.expect_ident_like()?;
10847                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10848                    if !matches!(self.peek(), Token::Eq) {
10849                        return Err(self.err(alloc::format!(
10850                            "expected '=' after hot_tier_bytes, got {:?}",
10851                            self.peek()
10852                        )));
10853                    }
10854                    self.advance();
10855                    let n = self.expect_u64_literal()?;
10856                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10857                }
10858                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10859                // accept-and-no-op for ALTER TABLE SET <subject>
10860                // forms that pg_dump emits but SPG either treats
10861                // as N/A (single-tenant, single-owner, no shared
10862                // tablespaces) or accepts the dump-side declaration
10863                // without runtime effect:
10864                //   SET SCHEMA <name>            (18.11)
10865                //   SET TABLESPACE <name>        (18.8)
10866                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10867                //   SET WITHOUT CLUSTER          (18.13)
10868                //   SET WITHOUT OIDS             (PG legacy)
10869                //   SET (option = value, …)      (storage parameters)
10870                //   SET REPLICA IDENTITY {…}     (18.14)
10871                if setting.eq_ignore_ascii_case("schema")
10872                    || setting.eq_ignore_ascii_case("tablespace")
10873                    || setting.eq_ignore_ascii_case("logged")
10874                    || setting.eq_ignore_ascii_case("unlogged")
10875                    || setting.eq_ignore_ascii_case("without")
10876                {
10877                    self.consume_until_statement_boundary();
10878                    return Ok(Vec::new());
10879                }
10880                if setting.eq_ignore_ascii_case("replica") {
10881                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10882                    self.consume_until_statement_boundary();
10883                    return Ok(Vec::new());
10884                }
10885                // SET (option=value, …) — storage parameters.
10886                if matches!(self.peek(), Token::LParen) {
10887                    self.consume_until_statement_boundary();
10888                    return Ok(Vec::new());
10889                }
10890                Err(self.err(alloc::format!(
10891                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10892                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10893                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10894                )))
10895            }
10896            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10897            // not ignored: round 645 gave SPG the inheritance the
10898            // v7.37.18 no-op said it did not have.
10899            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10900                self.advance();
10901                let parent = self.expect_ident_like()?;
10902                self.consume_until_statement_boundary();
10903                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10904                    parent,
10905                    detach: false
10906                }])
10907            }
10908            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10909            // LEVEL SECURITY`, which has its own RLS arm below — without
10910            // the guard this swallowed NO FORCE as a no-op.
10911            Token::Ident(s)
10912                if s.eq_ignore_ascii_case("no")
10913                    && !matches!(
10914                        self.tokens.get(self.pos + 1),
10915                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10916                    ) =>
10917            {
10918                self.advance();
10919                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10920                    if k.eq_ignore_ascii_case("inherit"))
10921                {
10922                    self.advance();
10923                    let parent = self.expect_ident_like()?;
10924                    self.consume_until_statement_boundary();
10925                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10926                        parent,
10927                        detach: true
10928                    }]);
10929                }
10930                self.consume_until_statement_boundary();
10931                Ok(Vec::new())
10932            }
10933            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10934            // single-owner, so there is still nothing to record.
10935            //
10936            // v7.39 (round 652) — but the name now reaches the engine,
10937            // which refuses a role that does not exist as PG does. The
10938            // no-op was swallowing the whole statement, so a dump naming
10939            // a role this server never heard of restored clean and left
10940            // the table owned by whoever ran the restore.
10941            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10942                self.advance();
10943                if matches!(self.peek(), Token::To) {
10944                    self.advance();
10945                }
10946                let role = self.expect_ident_like()?;
10947                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10948                    role
10949                }])
10950            }
10951            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10952            // PG sets a hint; SPG doesn't have clustered storage, so the
10953            // hint itself stays a no-op.
10954            //
10955            // v7.39 (round 652) — the index name is checked now. PG
10956            // errors on one that does not exist, and swallowing that let
10957            // a typo'd CLUSTER ON pass silently.
10958            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10959                self.advance();
10960                // `ON` is a reserved token, not an ident.
10961                if !matches!(self.peek(), Token::On) {
10962                    return Err(self.err(alloc::format!(
10963                        "expected ON after CLUSTER, got {:?}",
10964                        self.peek()
10965                    )));
10966                }
10967                self.advance();
10968                let index = self.expect_ident_like()?;
10969                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10970                    index: Some(index)
10971                }])
10972            }
10973            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10974            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10975            // what a logical decoder puts in the old-tuple image; SPG's
10976            // replication is SQL-text, so there is nothing to record.
10977            // Accept-and-no-op (it used to be a parse error).
10978            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10979                self.advance();
10980                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10981                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10982                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10983                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10984                {
10985                    self.advance(); // IDENTITY
10986                    self.advance(); // USING
10987                    if matches!(self.peek(), Token::Index)
10988                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10989                    {
10990                        self.advance();
10991                    }
10992                    let index = self.expect_ident_like()?;
10993                    self.consume_until_statement_boundary();
10994                    return Ok(alloc::vec![
10995                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10996                    ]);
10997                }
10998                self.consume_until_statement_boundary();
10999                Ok(Vec::new())
11000            }
11001            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11002            //
11003            // v7.39 (round 652) — it used to consume the statement and
11004            // return nothing, on the stated theory that SPG validated at
11005            // ADD CONSTRAINT time so there was never anything left to
11006            // validate. Measured against PG18, ADD CONSTRAINT did not
11007            // scan the existing rows at all — the comment described a
11008            // property SPG did not have, which is why nobody looked. Both
11009            // halves are real now: ADD scans unless told NOT VALID, and
11010            // this scans what NOT VALID skipped.
11011            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11012                self.advance();
11013                self.expect_keyword_ident("constraint")?;
11014                let name = self.expect_ident_like()?;
11015                Ok(alloc::vec![
11016                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11017                ])
11018            }
11019            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11020            // SET (option = value, …). PG uses it to clear per-table
11021            // storage params like fillfactor or autovacuum_*. SPG
11022            // engine-manages those parameters; accept-and-no-op.
11023            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11024                self.advance();
11025                self.consume_until_statement_boundary();
11026                Ok(Vec::new())
11027            }
11028            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11029            // type-of binding (PG 9.0+). SPG composite types
11030            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11031            // TABLE OF is rare and inverse of CREATE TABLE OF.
11032            // Accept-and-no-op until a customer dump round-trips it.
11033            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11034                self.advance();
11035                // v7.39 (round 710) — the type name is validated now.
11036                let type_name = self.expect_ident_like()?;
11037                self.consume_until_statement_boundary();
11038                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11039                    type_name
11040                }])
11041            }
11042            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11043            // (reserved keyword) rather than Token::Ident("not"),
11044            // so it needs its own arm. Accept-and-no-op same as OF.
11045            Token::Not => {
11046                self.advance();
11047                self.consume_until_statement_boundary();
11048                Ok(Vec::new())
11049            }
11050            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11051            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11052                self.advance();
11053                self.expect_row_level_security()?;
11054                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11055                    enabled: None,
11056                    force: Some(true),
11057                }])
11058            }
11059            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11060            Token::Ident(s)
11061                if s.eq_ignore_ascii_case("no")
11062                    && matches!(
11063                        self.tokens.get(self.pos + 1),
11064                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11065                    ) =>
11066            {
11067                self.advance(); // NO
11068                self.advance(); // FORCE
11069                self.expect_row_level_security()?;
11070                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11071                    enabled: None,
11072                    force: Some(false),
11073                }])
11074            }
11075            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11076            // (sets relrowsecurity). The guard requires the next token to be
11077            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11078            Token::Ident(s)
11079                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11080                    && matches!(
11081                        self.tokens.get(self.pos + 1),
11082                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11083                    ) =>
11084            {
11085                let enabled = s.eq_ignore_ascii_case("enable");
11086                self.advance(); // ENABLE/DISABLE
11087                self.expect_row_level_security()?;
11088                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11089                    enabled: Some(enabled),
11090                    force: None,
11091                }])
11092            }
11093            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11094                self.advance();
11095                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11096                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11097                // emits. The same grammar CREATE TABLE already accepts
11098                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11099                // through the SAME parser — an ALTER-only copy would be a
11100                // second place for the two to drift.
11101                if self.peek_mysql_inline_key_start() {
11102                    return Ok(match self.parse_mysql_inline_key()? {
11103                        Some(c) => {
11104                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11105                        }
11106                        // FULLTEXT / SPATIAL parse and are accepted as a
11107                        // no-op here exactly as they are inline.
11108                        None => Vec::new(),
11109                    });
11110                }
11111                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11112                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11113                // PRIMARY KEY this way; mysqldump emits both.
11114                // Peek-only dispatch (no advance) — `advance()`
11115                // destructively replaces consumed tokens with Eof,
11116                // so saved-pos restore would land on Eofs.
11117                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11118                {
11119                    // The next-but-one ident is the constraint
11120                    // name; the one after THAT is the kind.
11121                    let kind_pos = self.pos + 2;
11122                    let kind = self.tokens.get(kind_pos).cloned();
11123                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11124                    {
11125                        let fk = self.parse_table_level_fk()?;
11126                        return Ok(alloc::vec![
11127                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11128                        ]);
11129                    }
11130                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11131                    {
11132                        self.advance(); // CONSTRAINT
11133                        // v7.39 (read01 round 48) — keep the name; the engine
11134                        // stores it now instead of dropping it on the floor.
11135                        let con_name = self.expect_ident_like()?;
11136                        self.advance(); // PRIMARY
11137                        self.expect_keyword_ident("key")?;
11138                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11139                        // v7.39 (round 711) — the ALTER form carries the
11140                        // timing too (pg_dump writes it here).
11141                        let (deferrable, initially_deferred) =
11142                            self.consume_deferrable_clauses_timed()?;
11143                        return Ok(alloc::vec![
11144                            crate::ast::AlterTableTarget::AddTableConstraint(
11145                                crate::ast::TableConstraint::PrimaryKey {
11146                                    name: Some(con_name),
11147                                    columns: cols,
11148                                    deferrable,
11149                                    initially_deferred,
11150                                }
11151                            )
11152                        ]);
11153                    }
11154                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11155                    {
11156                        self.advance(); // CONSTRAINT
11157                        // v7.39 (read01 round 48) — keep the name.
11158                        let con_name = self.expect_ident_like()?;
11159                        // v7.22 (mailrs round-13 gap 6) — delegate so
11160                        // the optional `NULLS [NOT] DISTINCT` modifier
11161                        // parses here too (pg_dump emits the ALTER
11162                        // form; semantics enforced by the engine
11163                        // since v7.13).
11164                        let mut uc = self.parse_table_level_unique()?;
11165                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11166                            *name = Some(con_name);
11167                        }
11168                        return Ok(alloc::vec![
11169                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11170                        ]);
11171                    }
11172                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11173                    {
11174                        self.advance(); // CONSTRAINT
11175                        // v7.39 (read01 round 48) — keep the name.
11176                        let con_name = self.expect_ident_like()?;
11177                        self.advance(); // CHECK
11178                        if !matches!(self.peek(), Token::LParen) {
11179                            return Err(self.err(alloc::format!(
11180                                "expected '(' after CHECK, got {:?}", self.peek()
11181                            )));
11182                        }
11183                        self.advance();
11184                        let expr = self.parse_expr(0)?;
11185                        if matches!(self.peek(), Token::RParen) {
11186                            self.advance();
11187                        }
11188                        let not_valid = self.parse_not_valid_suffix();
11189                        return Ok(alloc::vec![
11190                            crate::ast::AlterTableTarget::AddTableConstraint(
11191                                crate::ast::TableConstraint::Check {
11192                                    name: Some(con_name),
11193                                    expr,
11194                                    not_valid,
11195                                }
11196                            )
11197                        ]);
11198                    }
11199                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11200                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11201                    // exclusion constraints via this ALTER form.
11202                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11203                    {
11204                        self.advance(); // CONSTRAINT
11205                        let con_name = self.expect_ident_like()?;
11206                        let mut ex = self.parse_table_level_exclude()?;
11207                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11208                            *name = Some(con_name);
11209                        }
11210                        return Ok(alloc::vec![
11211                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11212                        ]);
11213                    }
11214                    // Unknown kind — fall through to FK path which
11215                    // produces a descriptive parse error.
11216                }
11217                let is_fk = matches!(
11218                    self.peek(),
11219                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11220                        || s.eq_ignore_ascii_case("foreign")
11221                );
11222                if is_fk {
11223                    let fk = self.parse_table_level_fk()?;
11224                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11225                }
11226                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11227                // (no CONSTRAINT prefix) — same dispatch.
11228                match self.peek().clone() {
11229                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11230                        self.advance();
11231                        self.expect_keyword_ident("key")?;
11232                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11233                        let (deferrable, initially_deferred) =
11234                            self.consume_deferrable_clauses_timed()?;
11235                        return Ok(alloc::vec![
11236                            crate::ast::AlterTableTarget::AddTableConstraint(
11237                                crate::ast::TableConstraint::PrimaryKey {
11238                                    name: None,
11239                                    columns: cols,
11240                                    deferrable,
11241                                    initially_deferred,
11242                                }
11243                            )
11244                        ]);
11245                    }
11246                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11247                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11248                        let uc = self.parse_table_level_unique()?;
11249                        return Ok(alloc::vec![
11250                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11251                        ]);
11252                    }
11253                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11254                    // prefix). The other three bare forms were here and
11255                    // this one was not, so it fell through to the column
11256                    // path and came back as "unexpected reserved keyword
11257                    // 'check' at start of column definition".
11258                    _ if self.peek_table_level_check_start() => {
11259                        let chk = self.parse_table_level_check()?;
11260                        let not_valid = self.parse_not_valid_suffix();
11261                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11262                            unreachable!("parse_table_level_check returns Check")
11263                        };
11264                        return Ok(alloc::vec![
11265                            crate::ast::AlterTableTarget::AddTableConstraint(
11266                                crate::ast::TableConstraint::Check {
11267                                    name: None,
11268                                    expr,
11269                                    not_valid,
11270                                }
11271                            )
11272                        ]);
11273                    }
11274                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11275                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11276                        let ex = self.parse_table_level_exclude()?;
11277                        return Ok(alloc::vec![
11278                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11279                        ]);
11280                    }
11281                    _ => {}
11282                }
11283                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11284                    self.advance();
11285                }
11286                let mut if_not_exists = false;
11287                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11288                    self.advance();
11289                    if !matches!(self.peek(), Token::Not) {
11290                        return Err(self.err(alloc::format!(
11291                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11292                            self.peek()
11293                        )));
11294                    }
11295                    self.advance();
11296                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11297                        return Err(self.err(alloc::format!(
11298                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11299                            self.peek()
11300                        )));
11301                    }
11302                    self.advance();
11303                    if_not_exists = true;
11304                }
11305                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11306                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11307                // returns ColumnDef + an optional inline FK.
11308                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11309                let col_name = column.name.clone();
11310                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11311                    column,
11312                    if_not_exists,
11313                }];
11314                if let Some(mut fk) = col_level_fk {
11315                    if fk.columns.is_empty() {
11316                        fk.columns.push(col_name);
11317                    }
11318                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11319                }
11320                Ok(out)
11321            }
11322            Token::Drop => {
11323                self.advance();
11324                // v7.13.3 — dispatch on the next token. mailrs round-7
11325                // S8 closed DROP COLUMN; round-6 S7 closed
11326                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11327                // RESTRICT modifiers.
11328                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11329                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11330                let subject = match self.peek() {
11331                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11332                        self.advance();
11333                        "constraint"
11334                    }
11335                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11336                        self.advance();
11337                        "column"
11338                    }
11339                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11340                    // `INDEX` lexes as the reserved Token::Index, so it is
11341                    // unambiguous. `KEY` is a plain ident, and PG allows a
11342                    // column literally named "key", so only read it as the
11343                    // keyword when a name follows it.
11344                    Token::Index => {
11345                        self.advance();
11346                        "index"
11347                    }
11348                    Token::Ident(s)
11349                        if s.eq_ignore_ascii_case("key")
11350                            && matches!(
11351                                self.tokens.get(self.pos + 1),
11352                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11353                            ) =>
11354                    {
11355                        self.advance();
11356                        "index"
11357                    }
11358                    // PG-canonical bare `DROP <col>` without COLUMN
11359                    // keyword is also valid; treat any other ident
11360                    // as the column name.
11361                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11362                    other => {
11363                        return Err(self.err(alloc::format!(
11364                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11365                        )));
11366                    }
11367                };
11368                let mut if_exists = false;
11369                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11370                    let n1 = self.tokens.get(self.pos + 1);
11371                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11372                        self.advance();
11373                        self.advance();
11374                        if_exists = true;
11375                    }
11376                }
11377                let name = self.expect_ident_like()?;
11378                let mut cascade = false;
11379                if matches!(
11380                    self.peek(),
11381                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11382                        || s.eq_ignore_ascii_case("restrict")
11383                ) {
11384                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11385                    {
11386                        cascade = true;
11387                    }
11388                    self.advance();
11389                }
11390                if subject == "index" {
11391                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11392                        name,
11393                        if_exists,
11394                    }])
11395                } else if subject == "constraint" {
11396                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11397                        name,
11398                        if_exists,
11399                    }])
11400                } else {
11401                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11402                        column: name,
11403                        if_exists,
11404                        cascade,
11405                    }])
11406                }
11407            }
11408            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11409                self.advance();
11410                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11411                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11412                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11413                // immediately; accept-and-no-op.
11414                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11415                    self.advance();
11416                    self.consume_until_statement_boundary();
11417                    return Ok(Vec::new());
11418                }
11419                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11420                    self.advance();
11421                }
11422                let col_name = self.expect_ident_like()?;
11423                match self.peek() {
11424                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11425                        self.advance();
11426                    }
11427                    // v7.14.0 — pg_dump emits BIGSERIAL via
11428                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11429                    // nextval('seq')` (the sequence is created
11430                    // separately). SPG's BIGSERIAL already uses
11431                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11432                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11433                    // engine no-ops by consuming the tail.
11434                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11435                        // v7.22 (round-13 T2) — `SET DEFAULT
11436                        // nextval('…')` is how pg_dump spells a
11437                        // SERIAL column (plain integer in CREATE
11438                        // TABLE + this ALTER). It used to be
11439                        // swallowed as a no-op, which silently
11440                        // STRIPPED auto-increment from imported
11441                        // schemas — the first post-import INSERT
11442                        // without an explicit id then violated NOT
11443                        // NULL. Lower it to the auto-increment
11444                        // marker instead.
11445                        let is_default_nextval =
11446                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11447                                && matches!(
11448                                    self.tokens.get(self.pos + 2),
11449                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11450                                );
11451                        if is_default_nextval {
11452                            let seq_name = self.scan_sequence_name_until_boundary();
11453                            return Ok(alloc::vec![
11454                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11455                                    column: col_name,
11456                                    seq_name,
11457                                }
11458                            ]);
11459                        }
11460                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11461                        self.advance(); // consume "set"
11462                        match self.peek().clone() {
11463                            Token::Default => {
11464                                self.advance();
11465                                let default_expr = self.parse_expr(0)?;
11466                                return Ok(alloc::vec![
11467                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11468                                        column: col_name,
11469                                        default_expr,
11470                                    }
11471                                ]);
11472                            }
11473                            Token::Not => {
11474                                self.advance();
11475                                if !matches!(self.peek(), Token::Null) {
11476                                    return Err(self.err(alloc::format!(
11477                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11478                                        self.peek()
11479                                    )));
11480                                }
11481                                self.advance();
11482                                return Ok(alloc::vec![
11483                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11484                                        column: col_name,
11485                                    }
11486                                ]);
11487                            }
11488                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11489                            // stored generated column's expression and
11490                            // recompute existing rows.
11491                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11492                                self.advance(); // EXPRESSION
11493                                if matches!(self.peek(), Token::As) {
11494                                    self.advance();
11495                                }
11496                                let expr = self.parse_expr(0)?;
11497                                return Ok(alloc::vec![
11498                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11499                                        column: col_name,
11500                                        expr,
11501                                    }
11502                                ]);
11503                            }
11504                            other => {
11505                                // Other SET subjects (STATISTICS,
11506                                // STORAGE, COMPRESSION, …) stay no-ops —
11507                                // storage hints with no SPG semantics.
11508                                let _ = other;
11509                                self.consume_until_statement_boundary();
11510                                return Ok(Vec::new());
11511                            }
11512                        }
11513                    }
11514                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11515                        self.advance(); // consume "drop"
11516                        return self.parse_alter_column_drop_tail(col_name);
11517                    }
11518                    Token::Drop => {
11519                        self.advance(); // consume Drop token
11520                        return self.parse_alter_column_drop_tail(col_name);
11521                    }
11522                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11523                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11524                        // GENERATED { ALWAYS | BY DEFAULT } AS
11525                        // IDENTITY ( … )`: pg_dump's spelling for
11526                        // identity columns. Same auto-increment
11527                        // lowering as the nextval default; the
11528                        // sequence options inside the parens are
11529                        // no-ops under SPG's max+1 semantics.
11530                        let is_generated = matches!(
11531                            self.tokens.get(self.pos + 1),
11532                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11533                        );
11534                        if !is_generated {
11535                            return Err(self.err(alloc::format!(
11536                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11537                                self.tokens.get(self.pos + 1)
11538                            )));
11539                        }
11540                        let seq_name = self.scan_sequence_name_until_boundary();
11541                        return Ok(alloc::vec![
11542                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11543                                column: col_name,
11544                                seq_name,
11545                            }
11546                        ]);
11547                    }
11548                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11549                    // column: floor the next allocated value at n (bare
11550                    // RESTART = restart from the start value, 1).
11551                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11552                        self.advance();
11553                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11554                        {
11555                            self.advance();
11556                            let neg = if matches!(self.peek(), Token::Minus) {
11557                                self.advance();
11558                                true
11559                            } else {
11560                                false
11561                            };
11562                            match self.advance() {
11563                                Token::Integer(v) => Some(if neg { -v } else { v }),
11564                                other => {
11565                                    return Err(self.err(alloc::format!(
11566                                        "expected integer after RESTART WITH, got {other:?}"
11567                                    )));
11568                                }
11569                            }
11570                        } else {
11571                            None
11572                        };
11573                        return Ok(alloc::vec![
11574                            crate::ast::AlterTableTarget::AlterColumnRestart {
11575                                column: col_name,
11576                                with,
11577                            }
11578                        ]);
11579                    }
11580                    other => {
11581                        return Err(self.err(alloc::format!(
11582                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11583                        )));
11584                    }
11585                }
11586                // v7.39 (round 713) — the type parser has consumed a
11587                // trailing `COLLATE <name>` since Phase 2.5, and
11588                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11589                // TYPE text COLLATE "C"` parsed clean and changed
11590                // nothing. Keep the clause; the engine re-collates.
11591                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11592                    self.parse_type_with_implied_flags()?;
11593                let collation = if coll_explicit {
11594                    coll_name.map(|n| (coll, n))
11595                } else {
11596                    None
11597                };
11598                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11599                {
11600                    self.advance();
11601                    Some(self.parse_expr(0)?)
11602                } else {
11603                    None
11604                };
11605                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11606                    column: col_name,
11607                    new_type,
11608                    using,
11609                    collation,
11610                }])
11611            }
11612            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11613            // PG also supports `RENAME TO new_table` for table-name
11614            // rename; that surface is deferred (pg_dump never emits
11615            // it). If the first post-RENAME ident is `TO`, the user
11616            // is asking for table rename — error with a clear
11617            // message rather than misparsing `TO` as a column name.
11618            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11619                self.advance();
11620                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11621                // table-name rename (mailrs round-10 A.5 — used
11622                // by migrate-042's `RENAME TO email_contacts`).
11623                // `TO` lexes as Token::To.
11624                if matches!(self.peek(), Token::To)
11625                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11626                {
11627                    self.advance();
11628                    let new = self.expect_ident_like()?;
11629                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11630                        new,
11631                    }]);
11632                }
11633                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11634                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11635                    self.advance();
11636                    let old = self.expect_ident_like()?;
11637                    if matches!(self.peek(), Token::To) {
11638                        self.advance();
11639                    } else {
11640                        self.expect_keyword_ident("to")?;
11641                    }
11642                    let new = self.expect_ident_like()?;
11643                    return Ok(alloc::vec![
11644                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11645                    ]);
11646                }
11647                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11648                    self.advance();
11649                }
11650                let old = self.expect_ident_like()?;
11651                // `TO` is a reserved keyword token; accept both
11652                // Token::To and Token::Ident("to") for consistency.
11653                if matches!(self.peek(), Token::To) {
11654                    self.advance();
11655                } else {
11656                    self.expect_keyword_ident("to")?;
11657                }
11658                let new = self.expect_ident_like()?;
11659                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11660                    old,
11661                    new,
11662                }])
11663            }
11664            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11665            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11666            // every data block with these. Real disable semantics —
11667            // not no-op — because reload correctness assumes the
11668            // triggers don't fire (rows already carry their
11669            // computed values from prod).
11670            Token::Ident(s)
11671                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11672            {
11673                let enabled = s.eq_ignore_ascii_case("enable");
11674                self.advance();
11675                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11676                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11677                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11678                // pg_dump output) — anything else falls through to
11679                // the catch-all error below.
11680                // v7.22 (round-13 T3) — mysqldump wraps every data
11681                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11682                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11683                // maintains indexes incrementally — engine no-op.
11684                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11685                    self.advance();
11686                    return Ok(Vec::new());
11687                }
11688                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11689                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11690                // to gate triggers on session_replication_role; SPG
11691                // has no replica role, so the prefix is consumed and
11692                // treated identically to the plain ENABLE/DISABLE
11693                // TRIGGER form.
11694                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11695                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11696                {
11697                    self.advance();
11698                }
11699                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11700                    return Err(self.err(alloc::format!(
11701                        "expected TRIGGER after {}, got {:?}",
11702                        if enabled { "ENABLE" } else { "DISABLE" },
11703                        self.peek()
11704                    )));
11705                }
11706                self.advance();
11707                // `ALL` lexes as Token::All (reserved); also
11708                // accept Token::Ident("all") for symmetry.
11709                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11710                // TRIGGER selectors. USER (= all user triggers) is
11711                // semantically ALL here; REPLICA / ALWAYS gate on
11712                // session_replication_role which SPG doesn't track.
11713                // All map to TriggerSelector::All.
11714                let which = if matches!(self.peek(), Token::All)
11715                    || matches!(self.peek(), Token::Ident(s)
11716                        if s.eq_ignore_ascii_case("all")
11717                            || s.eq_ignore_ascii_case("user")
11718                            || s.eq_ignore_ascii_case("replica")
11719                            || s.eq_ignore_ascii_case("always"))
11720                {
11721                    self.advance();
11722                    crate::ast::TriggerSelector::All
11723                } else {
11724                    let name = self.expect_ident_like()?;
11725                    crate::ast::TriggerSelector::Named(name)
11726                };
11727                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11728                    which,
11729                    enabled,
11730                }])
11731            }
11732            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11733            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11734                self.advance();
11735                if !matches!(self.peek(), Token::Partition)
11736                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11737                        if s.eq_ignore_ascii_case("partition"))
11738                {
11739                    return Err(self.err(alloc::format!(
11740                        "expected PARTITION after ATTACH, got {:?}",
11741                        self.peek()
11742                    )));
11743                }
11744                self.advance();
11745                let child = self.expect_ident_like()?;
11746                let bounds = self.parse_partition_bounds_tail()?;
11747                Ok(alloc::vec![
11748                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11749                ])
11750            }
11751            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11752            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11753                self.advance();
11754                if !matches!(self.peek(), Token::Partition)
11755                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11756                        if s.eq_ignore_ascii_case("partition"))
11757                {
11758                    return Err(self.err(alloc::format!(
11759                        "expected PARTITION after DETACH, got {:?}",
11760                        self.peek()
11761                    )));
11762                }
11763                self.advance();
11764                let child = self.expect_ident_like()?;
11765                let mut concurrently = false;
11766                let mut finalize = false;
11767                loop {
11768                    match self.peek().clone() {
11769                        Token::Ident(s) | Token::QuotedIdent(s)
11770                            if s.eq_ignore_ascii_case("concurrently") =>
11771                        {
11772                            self.advance();
11773                            concurrently = true;
11774                        }
11775                        Token::Ident(s) | Token::QuotedIdent(s)
11776                            if s.eq_ignore_ascii_case("finalize") =>
11777                        {
11778                            self.advance();
11779                            finalize = true;
11780                        }
11781                        _ => break,
11782                    }
11783                }
11784                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11785                    child,
11786                    concurrently,
11787                    finalize,
11788                }])
11789            }
11790            other => Err(self.err(alloc::format!(
11791                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11792            ))),
11793        }
11794    }
11795
11796    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11797    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11798    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11799    /// `parse_partition_of_tail`'s bounds branch.
11800    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11801    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11802    /// lowering each to the respective AlterTableTarget. Any
11803    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11804    /// no-op via consume_until_statement_boundary.
11805    fn parse_alter_column_drop_tail(
11806        &mut self,
11807        col_name: String,
11808    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11809        match self.peek().clone() {
11810            Token::Default => {
11811                self.advance();
11812                Ok(alloc::vec![
11813                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11814                ])
11815            }
11816            Token::Not => {
11817                self.advance();
11818                if !matches!(self.peek(), Token::Null) {
11819                    return Err(self.err(alloc::format!(
11820                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11821                        self.peek()
11822                    )));
11823                }
11824                self.advance();
11825                Ok(alloc::vec![
11826                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11827                ])
11828            }
11829            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11830            // generated column into a plain column.
11831            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11832                self.advance();
11833                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11834                // dropped, so the engine still errored on a plain
11835                // column; PG's semantics are NOTICE + skip.
11836                let mut if_exists = false;
11837                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11838                    self.advance();
11839                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11840                        self.advance();
11841                        if_exists = true;
11842                    }
11843                }
11844                Ok(alloc::vec![
11845                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11846                        column: col_name,
11847                        if_exists,
11848                    }
11849                ])
11850            }
11851            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11852            // identity column into a plain column.
11853            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11854                self.advance();
11855                let mut if_exists = false;
11856                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11857                    self.advance();
11858                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11859                        self.advance();
11860                        if_exists = true;
11861                    }
11862                }
11863                Ok(alloc::vec![
11864                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11865                        column: col_name,
11866                        if_exists,
11867                    }
11868                ])
11869            }
11870            _ => {
11871                self.consume_until_statement_boundary();
11872                Ok(Vec::new())
11873            }
11874        }
11875    }
11876
11877    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11878    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11879    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11880    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11881    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11882        let mut opts = crate::ast::CopyOptions::default();
11883        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11884            return Ok(opts);
11885        }
11886        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11887            self.advance();
11888        }
11889        if matches!(self.peek(), Token::LParen) {
11890            self.advance();
11891            loop {
11892                self.parse_one_copy_option(&mut opts)?;
11893                match self.peek() {
11894                    Token::Comma => {
11895                        self.advance();
11896                    }
11897                    Token::RParen => {
11898                        self.advance();
11899                        break;
11900                    }
11901                    other => {
11902                        return Err(self.err(alloc::format!(
11903                            "expected ',' or ')' in COPY options, got {other:?}"
11904                        )));
11905                    }
11906                }
11907            }
11908        } else {
11909            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11910                self.parse_one_copy_option(&mut opts)?;
11911            }
11912        }
11913        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11914            return Err(self.err(alloc::format!(
11915                "unexpected token after COPY options: {:?}",
11916                self.peek()
11917            )));
11918        }
11919        Ok(opts)
11920    }
11921
11922    fn parse_one_copy_option(
11923        &mut self,
11924        opts: &mut crate::ast::CopyOptions,
11925    ) -> Result<(), ParseError> {
11926        use crate::ast::CopyFormat;
11927        // The option keyword. NULL lexes as its own token; the rest are
11928        // bare identifiers.
11929        let kw = match self.advance() {
11930            Token::Null => alloc::string::String::from("NULL"),
11931            Token::Ident(s) => s.to_uppercase(),
11932            other => {
11933                return Err(self.err(alloc::format!(
11934                    "expected a COPY option keyword, got {other:?}"
11935                )));
11936            }
11937        };
11938        match kw.as_str() {
11939            "FORMAT" => {
11940                let fmt = self.expect_ident_like()?;
11941                match fmt.to_ascii_uppercase().as_str() {
11942                    "CSV" => opts.format = CopyFormat::Csv,
11943                    "TEXT" => opts.format = CopyFormat::Text,
11944                    other => {
11945                        return Err(self.err(alloc::format!(
11946                            "COPY format \"{}\" not recognized",
11947                            other.to_ascii_lowercase()
11948                        )));
11949                    }
11950                }
11951            }
11952            // Legacy bare format keywords.
11953            "CSV" => opts.format = CopyFormat::Csv,
11954            "TEXT" => opts.format = CopyFormat::Text,
11955            "HEADER" => {
11956                opts.header = match self.peek() {
11957                    Token::True => {
11958                        self.advance();
11959                        true
11960                    }
11961                    Token::False => {
11962                        self.advance();
11963                        false
11964                    }
11965                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11966                        self.advance();
11967                        true
11968                    }
11969                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11970                        self.advance();
11971                        false
11972                    }
11973                    // Bare HEADER (no boolean) means HEADER true.
11974                    _ => true,
11975                };
11976            }
11977            // r1066 (7.38 S5.1) — pgbench 14+ loads with
11978            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
11979            // vacuum bookkeeping on a freshly created/truncated
11980            // table; SPG's per-statement visibility makes it a
11981            // faithful no-op, and rejecting it aborted `pgbench -i`
11982            // against the drop-in. Accept ON/OFF/bare, change nothing.
11983            "FREEZE" => match self.peek() {
11984                Token::True | Token::False => {
11985                    self.advance();
11986                }
11987                Token::Ident(s)
11988                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
11989                {
11990                    self.advance();
11991                }
11992                _ => {}
11993            },
11994            "DELIMITER" | "QUOTE" | "ESCAPE" => {
11995                let s = match self.advance() {
11996                    Token::String(s) => s,
11997                    other => {
11998                        return Err(self.err(alloc::format!(
11999                            "COPY {kw} expects a single-character string, got {other:?}"
12000                        )));
12001                    }
12002                };
12003                // v7.39 (round 247) — PG's wording (0A000), keyword in
12004                // lowercase: "COPY delimiter must be a single one-byte
12005                // character".
12006                let one_byte_err = || {
12007                    self.err(alloc::format!(
12008                        "COPY {} must be a single one-byte character",
12009                        kw.to_ascii_lowercase()
12010                    ))
12011                };
12012                let mut chars = s.chars();
12013                let c = chars.next().ok_or_else(one_byte_err)?;
12014                if chars.next().is_some() || c.len_utf8() != 1 {
12015                    return Err(one_byte_err());
12016                }
12017                match kw.as_str() {
12018                    "DELIMITER" => opts.delimiter = Some(c),
12019                    "QUOTE" => opts.quote = Some(c),
12020                    _ => opts.escape = Some(c),
12021                }
12022            }
12023            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12024            "FORCE_QUOTE" => {
12025                if matches!(self.peek(), Token::Star) {
12026                    self.advance();
12027                    opts.force_quote = Some(Vec::new());
12028                } else {
12029                    if !matches!(self.peek(), Token::LParen) {
12030                        return Err(self.err(alloc::format!(
12031                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12032                            self.peek()
12033                        )));
12034                    }
12035                    self.advance();
12036                    let mut cols = Vec::new();
12037                    loop {
12038                        cols.push(self.expect_ident_like()?);
12039                        match self.peek() {
12040                            Token::Comma => {
12041                                self.advance();
12042                            }
12043                            Token::RParen => {
12044                                self.advance();
12045                                break;
12046                            }
12047                            other => {
12048                                return Err(self.err(alloc::format!(
12049                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12050                                )));
12051                            }
12052                        }
12053                    }
12054                    opts.force_quote = Some(cols);
12055                }
12056            }
12057            "NULL" => {
12058                opts.null_str = Some(match self.advance() {
12059                    Token::String(s) => s,
12060                    other => {
12061                        return Err(self.err(alloc::format!(
12062                            "COPY NULL expects a quoted string, got {other:?}"
12063                        )));
12064                    }
12065                });
12066            }
12067            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12068            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12069            // FORCE_NULL too.
12070            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12071                let cols = self.parse_copy_column_list(&kw)?;
12072                if kw == "FORCE_NOT_NULL" {
12073                    opts.force_not_null = Some(cols);
12074                } else {
12075                    opts.force_null = Some(cols);
12076                }
12077            }
12078            other => {
12079                // PG's wording, lowercased option name.
12080                return Err(self.err(alloc::format!(
12081                    "option \"{}\" not recognized",
12082                    other.to_ascii_lowercase()
12083                )));
12084            }
12085        }
12086        Ok(())
12087    }
12088
12089    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12090    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12091    /// is the `*` spelling.
12092    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12093        if matches!(self.peek(), Token::Star) {
12094            self.advance();
12095            return Ok(Vec::new());
12096        }
12097        if !matches!(self.peek(), Token::LParen) {
12098            return Err(self.err(alloc::format!(
12099                "expected '(' or '*' after {kw}, got {:?}",
12100                self.peek()
12101            )));
12102        }
12103        self.advance();
12104        let mut cols = Vec::new();
12105        loop {
12106            cols.push(self.expect_ident_like()?);
12107            match self.peek() {
12108                Token::Comma => {
12109                    self.advance();
12110                }
12111                Token::RParen => {
12112                    self.advance();
12113                    break;
12114                }
12115                other => {
12116                    return Err(self.err(alloc::format!(
12117                        "expected ',' or ')' in {kw} list, got {other:?}"
12118                    )));
12119                }
12120            }
12121        }
12122        Ok(cols)
12123    }
12124
12125    fn parse_partition_bounds_tail(
12126        &mut self,
12127    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12128        use crate::ast::PartitionOfBoundsAst;
12129        match self.peek() {
12130            Token::Default => {
12131                self.advance();
12132                Ok(PartitionOfBoundsAst::Default)
12133            }
12134            Token::For => {
12135                self.advance();
12136                if !matches!(self.peek(), Token::Values) {
12137                    return Err(
12138                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12139                    );
12140                }
12141                self.advance();
12142                let want_with = matches!(
12143                    self.peek(),
12144                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12145                );
12146                if want_with {
12147                    self.advance();
12148                    if !matches!(self.peek(), Token::LParen) {
12149                        return Err(self.err(format!(
12150                            "expected '(' after FOR VALUES WITH, got {:?}",
12151                            self.peek()
12152                        )));
12153                    }
12154                    self.advance();
12155                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12156                    loop {
12157                        let key = self.expect_ident_like()?;
12158                        let n = match self.peek().clone() {
12159                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12160                                self.advance();
12161                                v as u32
12162                            }
12163                            other => {
12164                                return Err(self.err(format!(
12165                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12166                                )));
12167                            }
12168                        };
12169                        match key.to_ascii_uppercase().as_str() {
12170                            "MODULUS" => modulus = Some(n),
12171                            "REMAINDER" => remainder = Some(n),
12172                            other => {
12173                                return Err(self.err(format!(
12174                                    "FOR VALUES WITH: unknown key {other:?}; \
12175                                     expected MODULUS or REMAINDER"
12176                                )));
12177                            }
12178                        }
12179                        match self.peek() {
12180                            Token::Comma => {
12181                                self.advance();
12182                            }
12183                            Token::RParen => {
12184                                self.advance();
12185                                break;
12186                            }
12187                            other => {
12188                                return Err(self.err(format!(
12189                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12190                                )));
12191                            }
12192                        }
12193                    }
12194                    let modulus = modulus
12195                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12196                    let remainder = remainder.ok_or_else(|| {
12197                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12198                    })?;
12199                    if modulus == 0 {
12200                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12201                    }
12202                    if remainder >= modulus {
12203                        return Err(self.err(format!(
12204                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12205                        )));
12206                    }
12207                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12208                }
12209                match self.peek() {
12210                    Token::From => {
12211                        self.advance();
12212                        let lower = Box::new(self.parse_partition_bound_expr()?);
12213                        if !matches!(self.peek(), Token::To) {
12214                            return Err(self.err(format!(
12215                                "expected TO after FROM (...), got {:?}",
12216                                self.peek()
12217                            )));
12218                        }
12219                        self.advance();
12220                        let upper = Box::new(self.parse_partition_bound_expr()?);
12221                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12222                    }
12223                    Token::In => {
12224                        self.advance();
12225                        if !matches!(self.peek(), Token::LParen) {
12226                            return Err(self.err(format!(
12227                                "expected '(' after FOR VALUES IN, got {:?}",
12228                                self.peek()
12229                            )));
12230                        }
12231                        self.advance();
12232                        let mut values = Vec::new();
12233                        loop {
12234                            values.push(self.parse_expr(0)?);
12235                            match self.peek() {
12236                                Token::Comma => {
12237                                    self.advance();
12238                                }
12239                                Token::RParen => {
12240                                    self.advance();
12241                                    break;
12242                                }
12243                                other => {
12244                                    return Err(self.err(format!(
12245                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12246                                    )));
12247                                }
12248                            }
12249                        }
12250                        if values.is_empty() {
12251                            return Err(
12252                                self.err("FOR VALUES IN requires at least one literal".to_string())
12253                            );
12254                        }
12255                        Ok(PartitionOfBoundsAst::List { values })
12256                    }
12257                    other => Err(self.err(format!(
12258                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12259                    ))),
12260                }
12261            }
12262            other => Err(self.err(format!(
12263                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12264            ))),
12265        }
12266    }
12267
12268    /// v7.16.2 — peek for `information_schema.<tbl>` /
12269    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12270    /// three tokens + return a synthetic table name the engine's
12271    /// SELECT path recognises as a virtual view. Returns `None`
12272    /// when the head doesn't look like a meta-qualified name.
12273    /// Used by `parse_table_ref` to bypass the
12274    /// `expect_ident_like` schema-strip for these specific PG
12275    /// meta schemas (mailrs round-10 A.3).
12276    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12277        // Extract the schema name. Must be a plain ident token.
12278        let schema = match self.tokens.get(self.pos) {
12279            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12280            _ => return None,
12281        };
12282        // Dot.
12283        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12284            return None;
12285        }
12286        // The table-side ident may lex as a reserved keyword
12287        // (e.g. `Token::Tables`). Tolerate the common ones via a
12288        // helper that reads the trailing token's underlying name.
12289        let tbl = match self.tokens.get(self.pos + 2)? {
12290            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12291            Token::Tables => "tables".to_string(),
12292            // Other PG meta table names that may collide with
12293            // reserved keywords land here as needed.
12294            _ => return None,
12295        };
12296        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12297        // names so the synthetic name doesn't double-prefix
12298        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12299        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12300            ("__spg_info_", tbl.to_ascii_lowercase())
12301        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12302            // v7.39 (round 541) — only the catalogs SPG actually
12303            // synthesises are rewritten, which is what the BARE path
12304            // has always checked. Anything else keeps its own name and
12305            // takes the ordinary route: `pg_stat_activity` and friends
12306            // resolve through meta_view_result, and a name that is no
12307            // catalog at all gets PG's "relation does not exist"
12308            // instead of a message about a view SPG cannot materialise.
12309            let lowered = tbl.to_ascii_lowercase();
12310            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12311                self.advance(); // schema
12312                self.advance(); // dot
12313                self.advance(); // tbl
12314                return Some((lowered.clone(), lowered));
12315            }
12316            let bare = lowered
12317                .strip_prefix("pg_")
12318                .map(alloc::string::String::from)
12319                .unwrap_or(lowered);
12320            ("__spg_pg_", bare)
12321        } else if schema.eq_ignore_ascii_case("mysql") {
12322            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12323            // (`mysql.user`, `mysql.db`). Same synthetic-name
12324            // shape as pg_catalog.
12325            ("__spg_mysql_", tbl.to_ascii_lowercase())
12326        } else {
12327            return None;
12328        };
12329        self.advance(); // schema
12330        self.advance(); // dot
12331        self.advance(); // tbl
12332        Some((
12333            alloc::format!("{prefix}{normalised}"),
12334            tbl.to_ascii_lowercase(),
12335        ))
12336    }
12337
12338    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12339    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12340    /// implicit front of every search_path, so a bare reference to a
12341    /// known catalog table always means the catalog table. Only the
12342    /// names the engine actually synthesises are recognised — any
12343    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12344    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12345        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12346        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12347        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12348        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12349        // through the meta_view_result path instead, and already resolve
12350        // bare — they must NOT be listed here or the __spg_ rewrite would
12351        // mis-target them.)
12352        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12353        let name = match self.tokens.get(self.pos) {
12354            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12355            _ => return None,
12356        };
12357        // A following dot means this ident is a schema qualifier,
12358        // not a table name — let the qualified path handle it.
12359        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12360            return None;
12361        }
12362        if !PG_META_TABLES.contains(&name.as_str()) {
12363            return None;
12364        }
12365        self.advance();
12366        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12367        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12368    }
12369
12370    /// Consume a bare ident if its lowercase matches `kw`, else err.
12371    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12372    /// Peeks only; the caller advances.
12373    fn peek_keyword_ident(&self, kw: &str) -> bool {
12374        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12375    }
12376
12377    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12378        match self.advance() {
12379            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12380            other => Err(ParseError {
12381                message: format!("expected {kw:?}, got {other:?}"),
12382                token_pos: self.consumed_pos(),
12383            }),
12384        }
12385    }
12386
12387    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12388    /// literal (`'foo'`) — same shape used by CREATE USER for the
12389    /// username slot.
12390    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12391        match self.advance() {
12392            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12393            other => Err(ParseError {
12394                message: format!("expected identifier or string, got {other:?}"),
12395                token_pos: self.consumed_pos(),
12396            }),
12397        }
12398    }
12399
12400    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12401        match self.advance() {
12402            Token::String(s) => Ok(s),
12403            other => Err(ParseError {
12404                message: format!("expected quoted string, got {other:?}"),
12405                token_pos: self.consumed_pos(),
12406            }),
12407        }
12408    }
12409
12410    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12411        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12412        // subqueries recurse through here without passing
12413        // parse_expr; share the same nesting budget.
12414        self.enter_nested()?;
12415        let r = self.parse_select_stmt_inner();
12416        self.nest_depth -= 1;
12417        r
12418    }
12419
12420    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12421        // Caller dispatches on Token::Select; the inner helper handles
12422        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12423        // get a fresh bare-select parse and may not have their own ORDER
12424        // BY / LIMIT.
12425        let mut head = self.parse_bare_select()?;
12426        let into = self.pending_select_into.take();
12427        self.parse_setop_chain_into(&mut head)?;
12428        self.parse_select_tail_into(&mut head)?;
12429        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12430        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12431        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12432        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12433        // to the body, as it does in PostgreSQL.
12434        if let Some((name, temporary)) = into {
12435            return Ok(Statement::CreateMaterializedView(
12436                crate::ast::CreateMaterializedViewStatement {
12437                    temporary,
12438                    name,
12439                    if_not_exists: false,
12440                    columns: Vec::new(),
12441                    body: head,
12442                    with_data: true,
12443                    as_plain_table: true,
12444                },
12445            ));
12446        }
12447        Ok(Statement::Select(head))
12448    }
12449
12450    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12451    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12452    /// token), and INTERSECT [ALL] (a bare ident — it was never
12453    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12454    /// tighter than UNION / EXCEPT — the executor folds the chain
12455    /// left-to-right, which is already correct for LEADING
12456    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12457    /// pair nests into that previous peer, so A UNION B INTERSECT C
12458    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12459    /// groups.
12460    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12461        // A parenthesized group arrives with its own (already
12462        // regrouped) unions on `head`; only the pairs THIS chain
12463        // appends participate in the precedence regroup below —
12464        // nesting an outer INTERSECT into a group-internal peer
12465        // would dissolve the explicit grouping.
12466        let boundary = head.unions.len();
12467        loop {
12468            let base = match self.peek() {
12469                Token::Union => UnionKind::Distinct,
12470                Token::Except => UnionKind::Except,
12471                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12472                _ => break,
12473            };
12474            self.advance();
12475            let kind = if matches!(self.peek(), Token::All) {
12476                self.advance();
12477                match base {
12478                    UnionKind::Distinct => UnionKind::All,
12479                    UnionKind::Except => UnionKind::ExceptAll,
12480                    _ => UnionKind::IntersectAll,
12481                }
12482            } else {
12483                base
12484            };
12485            let peer = self.parse_bare_select()?;
12486            head.unions.push((kind, peer));
12487        }
12488        let mut pairs = core::mem::take(&mut head.unions);
12489        let tail = pairs.split_off(boundary);
12490        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12491        for (kind, peer) in tail {
12492            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12493            // An intersect nests into the previous element of THIS
12494            // chain only; with no new previous element it stays at
12495            // the outer level (the left fold applies it to the
12496            // whole head, group included).
12497            match (
12498                is_intersect,
12499                regrouped.len() > boundary,
12500                regrouped.last_mut(),
12501            ) {
12502                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12503                _ => regrouped.push((kind, peer)),
12504            }
12505        }
12506        head.unions = regrouped;
12507        Ok(())
12508    }
12509
12510    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12511    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12512    /// the top-level bare VALUES statement reuses it verbatim.
12513    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12514    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12515    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12516    /// where the grouping-set universe is still in scope.
12517    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12518        if !matches!(self.peek(), Token::Order) {
12519            return Ok(Vec::new());
12520        }
12521        self.advance();
12522        if !self.peek_is_by() {
12523            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12524        }
12525        self.advance();
12526        let mut keys = Vec::new();
12527        loop {
12528            // v7.39 (round 691) — save/restore, the discipline this parser
12529            // already uses around `pending_sample_preds`, so a subquery inside
12530            // a key neither inherits nor leaks the channel.
12531            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12532            let saved_coll = self.order_key_collation.take();
12533            let parsed = self.parse_expr(0);
12534            self.in_order_by_key = saved_flag;
12535            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12536            let expr = parsed?;
12537            let desc = if matches!(self.peek(), Token::Desc) {
12538                self.advance();
12539                true
12540            } else if matches!(self.peek(), Token::Asc) {
12541                self.advance();
12542                false
12543            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12544                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12545                // one ordering per type, so the btree comparison operators map
12546                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12547                // would need a custom operator class — honest error.
12548                self.advance();
12549                match self.advance() {
12550                    Token::Lt | Token::LtEq => false,
12551                    Token::Gt | Token::GtEq => true,
12552                    other => {
12553                        return Err(self.err(alloc::format!(
12554                            "ORDER BY USING supports the btree comparison \
12555                             operators (< <= > >=); got {other:?}"
12556                        )));
12557                    }
12558                }
12559            } else {
12560                false
12561            };
12562            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12563            let nulls_first = self.parse_optional_nulls_placement()?;
12564            keys.push(OrderBy {
12565                expr,
12566                desc,
12567                nulls_first,
12568                collation,
12569            });
12570            if matches!(self.peek(), Token::Comma) {
12571                self.advance();
12572            } else {
12573                break;
12574            }
12575        }
12576        Ok(keys)
12577    }
12578
12579    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12580        // v7.39 (round 135) — a grouping-set query may have already parsed +
12581        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12582        // no ORDER BY token is present, keep that pre-set order_by rather than
12583        // clobbering it with an empty list.
12584        let parsed_keys = self.parse_order_by_keys()?;
12585        head.order_by = if parsed_keys.is_empty() {
12586            core::mem::take(&mut head.order_by)
12587        } else {
12588            parsed_keys
12589        };
12590        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12591        // order. PG's grammar takes a limit clause and an offset clause
12592        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12593        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12594        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12595        // spelling died on `expected end of input, got Limit`.
12596        //
12597        // Each may appear at most once, and LIMIT and FETCH FIRST are
12598        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12599        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12600        // A second one is left unconsumed here, which the caller reports
12601        // as trailing input rather than silently taking the last.
12602        let mut saw_limit = false;
12603        let mut saw_offset = false;
12604        loop {
12605            if !saw_limit && matches!(self.peek(), Token::Limit) {
12606                self.advance();
12607                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12608                // PG synonyms for "no limit". Treat both as None
12609                // (no head.limit set) so the engine's existing
12610                // unlimited-result path takes over. Reject was the
12611                // pre-5.1 behaviour and broke pg_dump-flavoured
12612                // tooling that occasionally emits LIMIT NULL.
12613                if self.consume_limit_unbounded_sentinel() {
12614                    head.limit = None;
12615                } else {
12616                    let first = self.parse_limit_expr("LIMIT")?;
12617                    // MySQL `LIMIT offset, count` — the first number is
12618                    // the offset when a comma follows.
12619                    if matches!(self.peek(), Token::Comma) {
12620                        self.advance();
12621                        let count = self.parse_limit_expr("LIMIT")?;
12622                        head.offset = Some(first);
12623                        saw_offset = true;
12624                        head.limit = Some(count);
12625                    } else {
12626                        head.limit = Some(first);
12627                    }
12628                }
12629                saw_limit = true;
12630                continue;
12631            }
12632            if !saw_offset && matches!(self.peek(), Token::Offset) {
12633                self.advance();
12634                // PG also accepts an optional `ROW` / `ROWS` trailer
12635                // after the offset value (`OFFSET 10 ROWS`). The
12636                // FETCH-FIRST branch below relies on the same.
12637                let off = self.parse_limit_expr("OFFSET")?;
12638                self.consume_optional_rows_keyword();
12639                head.offset = Some(off);
12640                saw_offset = true;
12641                continue;
12642            }
12643            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12644            // the SQL-standard alias for LIMIT. PG accepts both
12645            // spellings interchangeably; pg_dump emits FETCH FIRST in
12646            // newer versions. We map it onto `head.limit` so the
12647            // engine path is unified.
12648            if !saw_limit
12649                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12650                    if s.eq_ignore_ascii_case("fetch"))
12651            {
12652                self.advance(); // FETCH
12653                // `FIRST` or `NEXT` (both legal per SQL standard).
12654                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12655                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12656                {
12657                    self.advance();
12658                }
12659                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12660                // implicit 1 — but we always consume one if present).
12661                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12662                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12663                {
12664                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12665                    crate::ast::LimitExpr::Literal(1)
12666                } else {
12667                    self.parse_limit_expr("FETCH FIRST")?
12668                };
12669                // Eat `ROW` / `ROWS` if not already consumed above.
12670                self.consume_optional_rows_keyword();
12671                // Optional `ONLY` (the spec form) — or the SQL:2008
12672                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12673                // now honours WITH TIES by extending past the LIMIT
12674                // truncation point through every row that shares the
12675                // last-kept row's ORDER BY key.
12676                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12677                    if s.eq_ignore_ascii_case("only"))
12678                {
12679                    self.advance();
12680                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12681                    if s.eq_ignore_ascii_case("with"))
12682                {
12683                    self.advance(); // WITH
12684                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12685                        if s.eq_ignore_ascii_case("ties"))
12686                    {
12687                        self.advance();
12688                        head.limit_with_ties = true;
12689                    }
12690                }
12691                head.limit = Some(count);
12692                saw_limit = true;
12693                continue;
12694            }
12695            break;
12696        }
12697        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12698        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12699        //       [ OF table_name [, …] ]
12700        //       [ NOWAIT | SKIP LOCKED ]
12701        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12702        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12703        // SELECT already returns a consistent snapshot — so these
12704        // are accept-and-discard: the parser absorbs them so
12705        // mailrs / Rails / Django code paths that emit `SELECT
12706        // … FOR UPDATE` for advisory pessimistic locking load
12707        // without a parser error. The on-disk locking model is
12708        // unchanged; callers that rely on FOR UPDATE for read-
12709        // through-write ordering still get the right answer
12710        // because SPG serialises writes anyway.
12711        head.locking = self
12712            .consume_optional_for_lock_clauses()
12713            .map(alloc::boxed::Box::new);
12714        Ok(())
12715    }
12716
12717    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12718    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12719    /// LOCKED ]` trailers. Each clause is fully accepted and
12720    /// discarded — SPG's single-writer model already satisfies the
12721    /// callers' implicit ordering requirement. Stops at the first
12722    /// token that isn't `FOR`.
12723    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12724        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12725        // not discarded. PG keeps the strongest of several clauses; the
12726        // policy of the last one wins, which is what this loop records.
12727        let mut seen: Option<crate::ast::LockingClause> = None;
12728        while matches!(self.peek(), Token::For) {
12729            // v7.37.14 (A2.5-stub) — record that this query asked
12730            // for a row lock the parser is about to silently
12731            // discard. Operators surface the count via
12732            // `spg_sql::silent_for_update_count()` so they can
12733            // gauge how much of the workload depends on advisory
12734            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12735            // before v7.37.15's per-row tuple locking lands.
12736            crate::record_silent_for_update_clause();
12737            self.advance(); // FOR
12738            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12739            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12740            let mut no_key = false;
12741            let mut key = false;
12742            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12743                if s.eq_ignore_ascii_case("no"))
12744            {
12745                self.advance(); // NO
12746                no_key = true;
12747                // The next ident should be KEY but be generous;
12748                // anything followed by UPDATE/SHARE is accepted.
12749                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12750                    if s.eq_ignore_ascii_case("key"))
12751                {
12752                    self.advance(); // KEY
12753                }
12754            }
12755            // `KEY` prefix (PG `FOR KEY SHARE`).
12756            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12757                if s.eq_ignore_ascii_case("key"))
12758            {
12759                self.advance(); // KEY
12760                key = true;
12761            }
12762            // Lock-strength keyword: UPDATE / SHARE. Required, but
12763            // we're lenient — an unexpected token here just bails
12764            // (we already consumed FOR; caller's downstream
12765            // dispatch will error if anything actually depends on
12766            // the trailing tokens).
12767            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12768                if s.eq_ignore_ascii_case("update"));
12769            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12770                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12771            {
12772                self.advance();
12773                use crate::ast::LockStrength as LS;
12774                let strength = match (is_update, no_key, key) {
12775                    (true, true, _) => LS::NoKeyUpdate,
12776                    (true, _, _) => LS::Update,
12777                    (false, _, true) => LS::KeyShare,
12778                    (false, _, _) => LS::Share,
12779                };
12780                seen = Some(crate::ast::LockingClause {
12781                    strength,
12782                    of_tables: alloc::vec::Vec::new(),
12783                    policy: crate::ast::LockWait::Wait,
12784                });
12785            } else {
12786                // FOR by itself (or `FOR KEY` with nothing after) —
12787                // give up on the lock-clause path. We've already
12788                // advanced past FOR; further attempts to parse
12789                // here would clobber state.
12790                return seen;
12791            }
12792            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12793            // joining and locking only a subset of tables.
12794            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12795                if s.eq_ignore_ascii_case("of"))
12796            {
12797                self.advance(); // OF
12798                #[allow(clippy::while_let_loop)]
12799                loop {
12800                    match self.peek() {
12801                        Token::Ident(_) | Token::QuotedIdent(_) => {
12802                            // v7.39 (round 294) — the name is CAPTURED now: PG
12803                            // validates it against the FROM clause, and an
12804                            // uncaptured list silently means "lock everything".
12805                            let mut nm = match self.advance() {
12806                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12807                                _ => alloc::string::String::new(),
12808                            };
12809                            // Optional schema-qualified `schema.table`.
12810                            if matches!(self.peek(), Token::Dot) {
12811                                self.advance();
12812                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12813                                {
12814                                    self.advance();
12815                                    nm = n;
12816                                }
12817                            }
12818                            if let Some(c) = seen.as_mut() {
12819                                c.of_tables.push(nm);
12820                            }
12821                        }
12822                        _ => break,
12823                    }
12824                    if matches!(self.peek(), Token::Comma) {
12825                        self.advance();
12826                    } else {
12827                        break;
12828                    }
12829                }
12830            }
12831            // Optional `NOWAIT` | `SKIP LOCKED`.
12832            match self.peek().clone() {
12833                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12834                    self.advance();
12835                    if let Some(c) = seen.as_mut() {
12836                        c.policy = crate::ast::LockWait::NoWait;
12837                    }
12838                }
12839                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12840                    self.advance(); // SKIP
12841                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12842                        if s.eq_ignore_ascii_case("locked"))
12843                    {
12844                        self.advance(); // LOCKED
12845                        if let Some(c) = seen.as_mut() {
12846                            c.policy = crate::ast::LockWait::SkipLocked;
12847                        }
12848                    }
12849                }
12850                _ => {}
12851            }
12852            // Loop: PG allows multiple FOR clauses chained.
12853        }
12854        seen
12855    }
12856
12857    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12858    /// Bind value gets resolved during prepared-statement Execute;
12859    /// the Pratt expression parser would over-accept here (e.g.
12860    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12861    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12862    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12863    /// when one was consumed; caller skips the regular
12864    /// limit-value parse and leaves `head.limit` at None.
12865    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12866        if matches!(self.peek(), Token::Null) {
12867            self.advance();
12868            return true;
12869        }
12870        if matches!(self.peek(), Token::All) {
12871            self.advance();
12872            return true;
12873        }
12874        false
12875    }
12876
12877    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12878    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12879    /// SQL-standard shape. No-op when missing.
12880    fn consume_optional_rows_keyword(&mut self) {
12881        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12882            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12883        {
12884            self.advance();
12885        }
12886    }
12887
12888    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12889    ///
12890    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12891    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12892    /// constant, which is why that spelling keeps the token path below.
12893    ///
12894    /// Constants are folded here rather than carried into the tree: the
12895    /// 15+ execution paths that read the row count go through
12896    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12897    /// means "no limit". A clause the engine could not resolve would
12898    /// therefore return the WHOLE table instead of failing. Folding at
12899    /// parse time keeps that impossible; a non-constant clause is still
12900    /// a clean error (recorded residual — closing it wants a resolution
12901    /// pre-pass on the simple-query path, where `substitute_placeholders`
12902    /// does not run).
12903    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12904        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12905        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12906        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12907        // ONLY` both work (its grammar takes a c_expr). Both measured
12908        // against PG 18.4 in round 305.
12909        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12910            return self.parse_limit_constant(label);
12911        }
12912        // One pass, no rewind: `advance()` takes each token by
12913        // `mem::replace`, so a consumed token reads back as Eof and this
12914        // parser cannot backtrack. Everything — bare literal included —
12915        // is therefore folded from the parsed expression rather than
12916        // re-read from the token stream.
12917        let start = self.pos;
12918        let e = self.parse_expr(0)?;
12919        if let crate::ast::Expr::Placeholder(n) = e {
12920            return Ok(crate::ast::LimitExpr::Placeholder(n));
12921        }
12922        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12923        match fold_limit_constant(&e) {
12924            Some(Ok(v)) if v < 0 => Err(ParseError {
12925                message: alloc::format!("{neg_label} must not be negative"),
12926                token_pos: start,
12927            }),
12928            Some(Ok(v)) => u32::try_from(v)
12929                .map(crate::ast::LimitExpr::Literal)
12930                .map_err(|_| ParseError {
12931                    message: alloc::format!("{label} value too large: {v}"),
12932                    token_pos: start,
12933                }),
12934            Some(Err(message)) => Err(ParseError {
12935                message: message.replace("{L}", neg_label),
12936                token_pos: start,
12937            }),
12938            // v7.39 (round 305, V23) — not foldable at parse time
12939            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12940            // expression; the engine evaluates it once before dispatch.
12941            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12942        }
12943    }
12944
12945    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12946        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12947        // coercion rules, not just an integer token: a NUMERIC rounds half
12948        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12949        // refused with PG's wording ("LIMIT must not be negative", 2201W /
12950        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12951        // content, failing as an input-syntax error on the value. General
12952        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12953        // they need an Expr-carrying LimitExpr variant.
12954        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12955        let err_at = |message: alloc::string::String, pos: usize| ParseError {
12956            message,
12957            token_pos: pos,
12958        };
12959        match self.advance() {
12960            Token::Integer(n) if n >= 0 => u32::try_from(n)
12961                .map(crate::ast::LimitExpr::Literal)
12962                .map_err(|_| ParseError {
12963                    message: alloc::format!("{label} value too large: {n}"),
12964                    token_pos: self.consumed_pos(),
12965                }),
12966            Token::Integer(_) => Err(err_at(
12967                alloc::format!("{neg_label} must not be negative"),
12968                self.pos.saturating_sub(1),
12969            )),
12970            Token::Numeric(t) => {
12971                let pos = self.pos.saturating_sub(1);
12972                let v: f64 = t.parse().map_err(|_| {
12973                    err_at(
12974                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12975                        pos,
12976                    )
12977                })?;
12978                if v < 0.0 {
12979                    return Err(err_at(
12980                        alloc::format!("{neg_label} must not be negative"),
12981                        pos,
12982                    ));
12983                }
12984                // Round half away from zero — PG's numeric→bigint cast.
12985                // (no_std: no f64::round; v is non-negative, so truncating
12986                // v + 0.5 is the same thing.)
12987                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12988                let rounded = (v + 0.5) as u64;
12989                u32::try_from(rounded)
12990                    .map(crate::ast::LimitExpr::Literal)
12991                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12992            }
12993            Token::Minus => {
12994                let pos = self.pos.saturating_sub(1);
12995                match self.peek() {
12996                    Token::Integer(_) | Token::Numeric(_) => {
12997                        self.advance();
12998                        Err(err_at(
12999                            alloc::format!("{neg_label} must not be negative"),
13000                            pos,
13001                        ))
13002                    }
13003                    other => Err(err_at(
13004                        alloc::format!(
13005                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13006                        ),
13007                        pos,
13008                    )),
13009                }
13010            }
13011            Token::String(t) => {
13012                let pos = self.pos.saturating_sub(1);
13013                match t.trim().parse::<i64>() {
13014                    Ok(n) if n < 0 => Err(err_at(
13015                        alloc::format!("{neg_label} must not be negative"),
13016                        pos,
13017                    )),
13018                    Ok(n) => u32::try_from(n)
13019                        .map(crate::ast::LimitExpr::Literal)
13020                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13021                    Err(_) => Err(err_at(
13022                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13023                        pos,
13024                    )),
13025                }
13026            }
13027            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13028            other => Err(ParseError {
13029                message: alloc::format!(
13030                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13031                ),
13032                token_pos: self.consumed_pos(),
13033            }),
13034        }
13035    }
13036
13037    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13038    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13039    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13040    /// `parse_select_stmt` is responsible for filling those in.
13041    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13042    /// call in the expression tree to the per-set integer bitmask
13043    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13044    /// is dropped in this grouping set). Runs during the ROLLUP /
13045    /// CUBE / GROUPING SETS expansion, where the set is known.
13046    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13047    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13048    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13049        if let Expr::FunctionCall { name, .. } = expr
13050            && name.eq_ignore_ascii_case("grouping")
13051        {
13052            if !out.iter().any(|e| e == expr) {
13053                out.push(expr.clone());
13054            }
13055            return;
13056        }
13057        match expr {
13058            Expr::Binary { lhs, rhs, .. } => {
13059                Self::collect_grouping_calls(lhs, out);
13060                Self::collect_grouping_calls(rhs, out);
13061            }
13062            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13063                Self::collect_grouping_calls(expr, out)
13064            }
13065            Expr::FunctionCall { args, .. } => {
13066                for a in args {
13067                    Self::collect_grouping_calls(a, out);
13068                }
13069            }
13070            Expr::Case {
13071                operand,
13072                branches,
13073                else_branch,
13074            } => {
13075                if let Some(o) = operand {
13076                    Self::collect_grouping_calls(o, out);
13077                }
13078                for (c, v) in branches {
13079                    Self::collect_grouping_calls(c, out);
13080                    Self::collect_grouping_calls(v, out);
13081                }
13082                if let Some(x) = else_branch {
13083                    Self::collect_grouping_calls(x, out);
13084                }
13085            }
13086            _ => {}
13087        }
13088    }
13089
13090    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13091    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13092    /// `__grp_ord_k` (injected per grouping-set branch).
13093    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13094        if let Expr::FunctionCall { name, .. } = expr
13095            && name.eq_ignore_ascii_case("grouping")
13096        {
13097            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13098                *expr = Expr::Column(crate::ast::ColumnName {
13099                    qualifier: None,
13100                    name: alloc::format!("__grp_ord_{k}"),
13101                });
13102            }
13103            return;
13104        }
13105        match expr {
13106            Expr::Binary { lhs, rhs, .. } => {
13107                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13108                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13109            }
13110            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13111                Self::rewrite_grouping_to_col(expr, grp_exprs)
13112            }
13113            Expr::FunctionCall { args, .. } => {
13114                for a in args {
13115                    Self::rewrite_grouping_to_col(a, grp_exprs);
13116                }
13117            }
13118            Expr::Case {
13119                operand,
13120                branches,
13121                else_branch,
13122            } => {
13123                if let Some(o) = operand {
13124                    Self::rewrite_grouping_to_col(o, grp_exprs);
13125                }
13126                for (c, v) in branches {
13127                    Self::rewrite_grouping_to_col(c, grp_exprs);
13128                    Self::rewrite_grouping_to_col(v, grp_exprs);
13129                }
13130                if let Some(x) = else_branch {
13131                    Self::rewrite_grouping_to_col(x, grp_exprs);
13132                }
13133            }
13134            _ => {}
13135        }
13136    }
13137
13138    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13139    /// as the list of key sets it contributes. A bare expression is one
13140    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13141    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13142    /// the concatenation of its items' sets, where an item is itself an
13143    /// element, a parenthesized key list, or the empty set `()`. A
13144    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13145    /// move together.
13146    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13147        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13148        // ROLLUP ( … ) / CUBE ( … )
13149        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13150            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13151        {
13152            let is_cube = is_kw(self.peek(), "cube");
13153            self.advance(); // ROLLUP / CUBE
13154            self.advance(); // (
13155            let mut units: Vec<Vec<Expr>> = Vec::new();
13156            loop {
13157                if matches!(self.peek(), Token::LParen) {
13158                    // Composite unit: (a, b) rolls up as one.
13159                    self.advance();
13160                    let mut unit = Vec::new();
13161                    if !matches!(self.peek(), Token::RParen) {
13162                        loop {
13163                            unit.push(self.parse_expr(0)?);
13164                            match self.peek() {
13165                                Token::Comma => {
13166                                    self.advance();
13167                                }
13168                                Token::RParen => break,
13169                                other => {
13170                                    return Err(self.err(format!(
13171                                        "expected ',' or ')' in grouping unit, got {other:?}"
13172                                    )));
13173                                }
13174                            }
13175                        }
13176                    }
13177                    self.advance(); // )
13178                    units.push(unit);
13179                } else {
13180                    units.push(alloc::vec![self.parse_expr(0)?]);
13181                }
13182                match self.peek() {
13183                    Token::Comma => {
13184                        self.advance();
13185                    }
13186                    Token::RParen => break,
13187                    other => {
13188                        return Err(self.err(format!(
13189                            "expected ',' or ')' in grouping list, got {other:?}"
13190                        )));
13191                    }
13192                }
13193            }
13194            self.advance(); // )
13195            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13196                units
13197                    .iter()
13198                    .zip(unit_sel.iter())
13199                    .filter(|(_, keep)| **keep)
13200                    .flat_map(|(u, _)| u.iter().cloned())
13201                    .collect()
13202            };
13203            let n = units.len();
13204            if is_cube {
13205                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13206                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13207                    .collect();
13208                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13209                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13210            }
13211            return Ok((0..=n)
13212                .rev()
13213                .map(|keep| {
13214                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13215                    flatten(&sel)
13216                })
13217                .collect());
13218        }
13219        // GROUPING SETS ( item [, item]* )
13220        if is_kw(self.peek(), "grouping")
13221            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13222        {
13223            self.advance(); // GROUPING
13224            self.advance(); // SETS
13225            if !matches!(self.peek(), Token::LParen) {
13226                return Err(self.err(format!(
13227                    "expected '(' after GROUPING SETS, got {:?}",
13228                    self.peek()
13229                )));
13230            }
13231            self.advance(); // outer (
13232            let mut sets: Vec<Vec<Expr>> = Vec::new();
13233            loop {
13234                if matches!(self.peek(), Token::LParen) {
13235                    // A parenthesized key list (or the empty set).
13236                    self.advance();
13237                    let mut set = Vec::new();
13238                    if !matches!(self.peek(), Token::RParen) {
13239                        loop {
13240                            set.push(self.parse_expr(0)?);
13241                            match self.peek() {
13242                                Token::Comma => {
13243                                    self.advance();
13244                                }
13245                                Token::RParen => break,
13246                                other => {
13247                                    return Err(self.err(format!(
13248                                        "expected ',' or ')' in grouping set, got {other:?}"
13249                                    )));
13250                                }
13251                            }
13252                        }
13253                    }
13254                    self.advance(); // )
13255                    sets.push(set);
13256                } else {
13257                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13258                    // bare expression.
13259                    sets.extend(self.parse_grouping_element()?);
13260                }
13261                match self.peek() {
13262                    Token::Comma => {
13263                        self.advance();
13264                    }
13265                    Token::RParen => break,
13266                    other => {
13267                        return Err(self.err(format!(
13268                            "expected ',' or ')' after a grouping set, got {other:?}"
13269                        )));
13270                    }
13271                }
13272            }
13273            self.advance(); // outer )
13274            return Ok(sets);
13275        }
13276        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13277    }
13278
13279    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13280        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13281        // set evaluates to NULL, at any depth. Previously only a *top-level*
13282        // select item equal to a dropped key was nullified, so a key nested in
13283        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13284        // column and failed to resolve against the set's synthetic schema.
13285        if dropped.iter().any(|d| d == expr) {
13286            *expr = Expr::Literal(Literal::Null);
13287            return;
13288        }
13289        if let Expr::FunctionCall { name, args } = expr
13290            && name.eq_ignore_ascii_case("grouping")
13291        {
13292            let mut mask: i64 = 0;
13293            for a in args.iter() {
13294                mask <<= 1;
13295                if dropped.iter().any(|d| d == a) {
13296                    mask |= 1;
13297                }
13298            }
13299            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13300            // literal: a bare integer in a select item is indistinguishable
13301            // from a positional reference once `ORDER BY 1` substitutes the
13302            // item back in, and the round-232 position check then read the
13303            // mask value as an out-of-range position. The cast changes
13304            // nothing semantically (grouping() is integer).
13305            *expr = Expr::Cast {
13306                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13307                target: crate::ast::CastTarget::Int,
13308            };
13309            return;
13310        }
13311        // Generic recursion over the common expression shapes the
13312        // SELECT list uses; anything without child expressions is
13313        // left alone.
13314        match expr {
13315            Expr::FunctionCall { args, .. } => {
13316                for a in args {
13317                    Self::substitute_grouping_calls(a, dropped);
13318                }
13319            }
13320            Expr::Binary { lhs, rhs, .. } => {
13321                Self::substitute_grouping_calls(lhs, dropped);
13322                Self::substitute_grouping_calls(rhs, dropped);
13323            }
13324            Expr::Unary { expr: inner, .. } => {
13325                Self::substitute_grouping_calls(inner, dropped);
13326            }
13327            Expr::Cast { expr: inner, .. } => {
13328                Self::substitute_grouping_calls(inner, dropped);
13329            }
13330            Expr::Case {
13331                operand,
13332                branches,
13333                else_branch,
13334            } => {
13335                if let Some(op) = operand {
13336                    Self::substitute_grouping_calls(op, dropped);
13337                }
13338                for (w, t) in branches {
13339                    Self::substitute_grouping_calls(w, dropped);
13340                    Self::substitute_grouping_calls(t, dropped);
13341                }
13342                if let Some(e) = else_branch {
13343                    Self::substitute_grouping_calls(e, dropped);
13344                }
13345            }
13346            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13347            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13348            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13349            // …` is the canonical rollup-total label idiom).
13350            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13351            Expr::Like { expr, pattern, .. } => {
13352                Self::substitute_grouping_calls(expr, dropped);
13353                Self::substitute_grouping_calls(pattern, dropped);
13354            }
13355            Expr::InList { expr, list, .. } => {
13356                Self::substitute_grouping_calls(expr, dropped);
13357                for item in list {
13358                    Self::substitute_grouping_calls(item, dropped);
13359                }
13360            }
13361            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13362            Expr::Array(items) => {
13363                for item in items {
13364                    Self::substitute_grouping_calls(item, dropped);
13365                }
13366            }
13367            Expr::ArraySubscript { target, index } => {
13368                Self::substitute_grouping_calls(target, dropped);
13369                Self::substitute_grouping_calls(index, dropped);
13370            }
13371            Expr::ArraySlice { target, lo, hi } => {
13372                Self::substitute_grouping_calls(target, dropped);
13373                if let Some(lo) = lo {
13374                    Self::substitute_grouping_calls(lo, dropped);
13375                }
13376                if let Some(hi) = hi {
13377                    Self::substitute_grouping_calls(hi, dropped);
13378                }
13379            }
13380            Expr::AnyAll { expr, array, .. } => {
13381                Self::substitute_grouping_calls(expr, dropped);
13382                Self::substitute_grouping_calls(array, dropped);
13383            }
13384            _ => {}
13385        }
13386    }
13387
13388    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13389        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13390        // group: `( <select chain> )` usable anywhere a query block
13391        // is (head or peer of an outer chain). The group's own
13392        // unions ride the returned SelectStatement; the executor's
13393        // nested-peer recursion runs them.
13394        if matches!(self.peek(), Token::LParen)
13395            && matches!(
13396                self.tokens.get(self.pos + 1),
13397                Some(Token::Select | Token::LParen | Token::Values)
13398            )
13399        {
13400            self.advance(); // (
13401            self.enter_nested()?;
13402            // v7.37 D.20 — a group whose head is a VALUES list:
13403            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13404            // otherwise recurse into a nested SELECT/group head.
13405            let mut head = (if matches!(self.peek(), Token::Values) {
13406                self.advance(); // VALUES
13407                self.parse_values_rows_body()
13408            } else {
13409                self.parse_bare_select()
13410            })
13411            .and_then(|mut h| {
13412                self.parse_setop_chain_into(&mut h)?;
13413                Ok(h)
13414            });
13415            self.nest_depth -= 1;
13416            let mut head = match &mut head {
13417                Ok(h) => core::mem::take(h),
13418                Err(_) => return head,
13419            };
13420            // v7.37.17 (17.6 siblings) — group-internal tail:
13421            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13422            // group head, then wrap the group as a derived table
13423            // (SELECT * FROM (group)) so the outer chain / outer
13424            // tail can't clobber the group's own ordering or limit.
13425            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13426                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13427                    if s.eq_ignore_ascii_case("fetch"));
13428            if has_tail {
13429                self.parse_select_tail_into(&mut head)?;
13430                head = SelectStatement {
13431                    locking: None,
13432                    ctes: Vec::new(),
13433                    distinct: false,
13434                    distinct_on: Vec::new(),
13435                    items: alloc::vec![SelectItem::Wildcard],
13436                    from: Some(FromClause {
13437                        primary: TableRef {
13438                            name: "subquery".to_string(),
13439                            alias: None,
13440                            only: false,
13441                            as_of_segment: None,
13442                            unnest_expr: None,
13443                            unnest_column_aliases: Vec::new(),
13444                            with_ordinality: false,
13445                            generate_series_args: None,
13446                            lateral_subquery: Some(Box::new(head)),
13447                            jsonb_each_text_arg: None,
13448                            table_fn_call: None,
13449                            rows_from: None,
13450                            json_table: None,
13451                            scalar_fn_item: false,
13452                        },
13453                        joins: Vec::new(),
13454                    }),
13455                    where_: None,
13456                    group_by: None,
13457                    group_by_all: false,
13458                    having: None,
13459                    unions: Vec::new(),
13460                    order_by: Vec::new(),
13461                    limit: None,
13462                    offset: None,
13463                    limit_with_ties: false,
13464                    window_check_exprs: Vec::new(),
13465                };
13466            }
13467            if !matches!(self.peek(), Token::RParen) {
13468                return Err(self.err(format!(
13469                    "expected ')' after parenthesized query group, got {:?}",
13470                    self.peek()
13471                )));
13472            }
13473            self.advance();
13474            return Ok(head);
13475        }
13476        // `TABLE name` shorthand as a query block — valid anywhere
13477        // a SELECT head is (set-op peers included).
13478        if matches!(self.peek(), Token::Table)
13479            && matches!(
13480                self.tokens.get(self.pos + 1),
13481                Some(Token::Ident(_) | Token::QuotedIdent(_))
13482            )
13483        {
13484            return self.parse_table_shorthand();
13485        }
13486        if !matches!(self.peek(), Token::Select) {
13487            return Err(self.err(format!(
13488                "expected SELECT to start a query block, got {:?}",
13489                self.peek()
13490            )));
13491        }
13492        self.advance();
13493        let distinct = if matches!(self.peek(), Token::Distinct) {
13494            self.advance();
13495            true
13496        } else {
13497            false
13498        };
13499        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13500        // keep the first row (per ORDER BY) of each group the
13501        // expressions define. Django's .distinct('field') shape.
13502        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13503            self.advance(); // ON
13504            if !matches!(self.peek(), Token::LParen) {
13505                return Err(self.err(format!(
13506                    "expected '(' after DISTINCT ON, got {:?}",
13507                    self.peek()
13508                )));
13509            }
13510            self.advance();
13511            let mut exprs = Vec::new();
13512            loop {
13513                exprs.push(self.parse_expr(0)?);
13514                match self.peek() {
13515                    Token::Comma => {
13516                        self.advance();
13517                    }
13518                    Token::RParen => break,
13519                    other => {
13520                        return Err(self.err(format!(
13521                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13522                        )));
13523                    }
13524                }
13525            }
13526            self.advance(); // )
13527            exprs
13528        } else {
13529            Vec::new()
13530        };
13531        let mut items = self.parse_select_list()?;
13532        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13533        // of CTAS. It sits exactly here in PG's grammar, right after the
13534        // target list.
13535        //
13536        // A comment in `ast.rs` has said since v7.38 that CTAS and
13537        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13538        // `SELECT i INTO t FROM src` answered `syntax error at or near
13539        // "INTO"`, which the differential found while measuring what
13540        // PostgreSQL tags each of the five materialising forms with. A
13541        // comment describing a capability the code does not have is the
13542        // defect this version has been finding all day, and this is the
13543        // one it found in the parser.
13544        //
13545        // `INTO` is captured rather than consumed here: the name has to
13546        // travel out of a function that returns a `SelectStatement`, and
13547        // the caller lowers the whole thing to the CTAS node.
13548        if matches!(self.peek(), Token::Into) {
13549            self.advance();
13550            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13551            // the target, not part of its name. SPG has one storage
13552            // class, so `UNLOGGED` is accepted and means nothing, which
13553            // is what it already means on `CREATE TABLE`.
13554            let mut temporary = false;
13555            loop {
13556                match self.peek().clone() {
13557                    Token::Ident(w) | Token::QuotedIdent(w)
13558                        if w.eq_ignore_ascii_case("temp")
13559                            || w.eq_ignore_ascii_case("temporary") =>
13560                    {
13561                        temporary = true;
13562                        self.advance();
13563                    }
13564                    Token::Ident(w) | Token::QuotedIdent(w)
13565                        if w.eq_ignore_ascii_case("unlogged") =>
13566                    {
13567                        self.advance();
13568                    }
13569                    Token::Table => {
13570                        self.advance();
13571                    }
13572                    _ => break,
13573                }
13574            }
13575            let name = match self.peek().clone() {
13576                Token::Ident(w) | Token::QuotedIdent(w) => {
13577                    self.advance();
13578                    w
13579                }
13580                other => {
13581                    return Err(self.err(alloc::format!(
13582                        "expected a table name after SELECT … INTO, got {other:?}"
13583                    )));
13584                }
13585            };
13586            self.pending_select_into = Some((name, temporary));
13587        }
13588        // Scope the TABLESAMPLE lowering channel to this SELECT:
13589        // stash whatever an enclosing select accumulated, collect
13590        // our own FROM's predicates, restore after the combine.
13591        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13592        let mut from = if matches!(self.peek(), Token::From) {
13593            self.advance();
13594            Some(self.parse_from_clause()?)
13595        } else {
13596            None
13597        };
13598        // v7.37 D.22 — a set-returning function in the projection with no FROM
13599        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13600        // rows. Move the first SRF projection item to a FROM-position derived
13601        // table and replace it in the projection with a reference to its output
13602        // column; sibling scalar columns repeat per SRF row. PG names the output
13603        // column after the function (or its AS alias). Reuses the FROM-SRF
13604        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13605        // works via the targetlist-SRF path.
13606        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13607        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13608        // is exactly what the function's own row shape already is. Anywhere else
13609        // (per outer row, or beside other items) it would need a real record-typed
13610        // projection, so it says so rather than answering something else.
13611        if let [
13612            SelectItem::Expr {
13613                expr: Expr::FunctionCall { name, args },
13614                ..
13615            },
13616        ] = items.as_slice()
13617            && name == "__record_expand"
13618        {
13619            let Some(Expr::FunctionCall {
13620                name: inner_name,
13621                args: inner_args,
13622            }) = args.first()
13623            else {
13624                return Err(self.err(
13625                    "(<expr>).* expands a function's record — it needs a function call".into(),
13626                ));
13627            };
13628            if from.is_some() {
13629                return Err(self.err(
13630                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13631                        .into(),
13632                ));
13633            }
13634            let fn_ref = TableRef {
13635                name: inner_name.clone(),
13636                alias: None,
13637                only: false,
13638                as_of_segment: None,
13639                unnest_expr: None,
13640                unnest_column_aliases: Vec::new(),
13641                with_ordinality: false,
13642                generate_series_args: None,
13643                lateral_subquery: None,
13644                jsonb_each_text_arg: None,
13645                table_fn_call: Some(Box::new((
13646                    inner_name.to_ascii_lowercase(),
13647                    inner_args.clone(),
13648                ))),
13649                rows_from: None,
13650                json_table: None,
13651                scalar_fn_item: false,
13652            };
13653            items = alloc::vec![SelectItem::Wildcard];
13654            from = Some(FromClause {
13655                primary: fn_ref,
13656                joins: Vec::new(),
13657            });
13658        }
13659        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13660        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13661        // record's fields takes the catalog. It becomes a LATERAL of the same
13662        // function plus one item per declared column — the machinery rounds 65
13663        // and 69 already built.
13664        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13665        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13666        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13667        // express, since the lifted one becomes a scan and the other would
13668        // expand per its rows (a cross product, not a zip). So when the
13669        // projection holds more than one top-level function call, the lift steps
13670        // aside and the engine's target-list expansion takes the whole list.
13671        let fn_call_items = items
13672            .iter()
13673            .filter(|it| {
13674                matches!(
13675                    it,
13676                    SelectItem::Expr {
13677                        expr: Expr::FunctionCall { .. },
13678                        ..
13679                    }
13680                )
13681            })
13682            .count();
13683        if from.is_none() && fn_call_items <= 1 {
13684            let mut found: Option<(usize, TableRef, String)> = None;
13685            for (i, item) in items.iter().enumerate() {
13686                if let SelectItem::Expr {
13687                    expr: Expr::FunctionCall { name, args },
13688                    alias,
13689                } = item
13690                {
13691                    let lname = name.to_ascii_lowercase();
13692                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13693                    let (unnest, gs) = match lname.as_str() {
13694                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13695                        "generate_series" if (2..=3).contains(&args.len()) => {
13696                            (None, Some(args.clone()))
13697                        }
13698                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13699                        // no-FROM projection yields the 1-based subscripts, i.e.
13700                        // generate_series(1, array_length(arr, dim)); an invalid
13701                        // dimension makes array_length NULL → 0 rows, as in PG.
13702                        "generate_subscripts" if args.len() == 2 => (
13703                            None,
13704                            Some(alloc::vec![
13705                                Expr::Literal(Literal::Integer(1)),
13706                                Expr::FunctionCall {
13707                                    name: "array_length".to_string(),
13708                                    args: args.clone(),
13709                                },
13710                            ]),
13711                        ),
13712                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13713                        // in a no-FROM projection unnest their *_to_array form.
13714                        "string_to_table" | "regexp_split_to_table" => {
13715                            let array_fn = if lname == "string_to_table" {
13716                                "string_to_array"
13717                            } else {
13718                                "regexp_split_to_array"
13719                            };
13720                            (
13721                                Some(Box::new(Expr::FunctionCall {
13722                                    name: array_fn.to_string(),
13723                                    args: args.clone(),
13724                                })),
13725                                None,
13726                            )
13727                        }
13728                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13729                        // a no-FROM projection expand per element. The scalar form
13730                        // returns the elements as a TEXT array, so unnest over the
13731                        // same call materialises one row each (same rewrite the
13732                        // FROM-clause form uses).
13733                        "jsonb_array_elements"
13734                        | "json_array_elements"
13735                        | "jsonb_array_elements_text"
13736                        | "json_array_elements_text"
13737                            if args.len() == 1 =>
13738                        {
13739                            (
13740                                Some(Box::new(Expr::FunctionCall {
13741                                    name: lname.clone(),
13742                                    args: args.clone(),
13743                                })),
13744                                None,
13745                            )
13746                        }
13747                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13748                        // in a no-FROM projection expands per match (scalar form
13749                        // returns the matches as a TEXT array → unnest).
13750                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13751                            Some(Box::new(Expr::FunctionCall {
13752                                name: lname.clone(),
13753                                args: args.clone(),
13754                            })),
13755                            None,
13756                        ),
13757                        _ => continue,
13758                    };
13759                    found = Some((
13760                        i,
13761                        TableRef {
13762                            name: colname.clone(),
13763                            alias: Some(colname.clone()),
13764                            only: false,
13765                            as_of_segment: None,
13766                            unnest_expr: unnest,
13767                            unnest_column_aliases: alloc::vec![colname.clone()],
13768                            with_ordinality: false,
13769                            generate_series_args: gs,
13770                            lateral_subquery: None,
13771                            jsonb_each_text_arg: None,
13772                            table_fn_call: None,
13773                            rows_from: None,
13774                            json_table: None,
13775                            scalar_fn_item: false,
13776                        },
13777                        colname,
13778                    ));
13779                    break;
13780                }
13781            }
13782            if let Some((idx, tref, colname)) = found {
13783                from = Some(FromClause {
13784                    primary: tref,
13785                    joins: Vec::new(),
13786                });
13787                items[idx] = SelectItem::Expr {
13788                    expr: Expr::Column(ColumnName {
13789                        qualifier: None,
13790                        name: colname.clone(),
13791                    }),
13792                    alias: Some(colname),
13793                };
13794            }
13795        }
13796        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13797        let where_ = if matches!(self.peek(), Token::Where) {
13798            self.advance();
13799            Some(self.parse_expr(0)?)
13800        } else {
13801            None
13802        };
13803        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13804            Some(match acc {
13805                Some(w) => Expr::Binary {
13806                    lhs: Box::new(pred),
13807                    op: crate::ast::BinOp::And,
13808                    rhs: Box::new(w),
13809                },
13810                None => pred,
13811            })
13812        });
13813        self.pending_sample_preds = enclosing_sample_preds;
13814        let mut group_by_all = false;
13815        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13816        // share one expansion: `grouping_sets` lists the key subsets
13817        // (first = primary, assigned to stmt.group_by; the rest
13818        // become UNION ALL peers), `grouping_universe` is the full
13819        // key list used to compute each peer's dropped keys.
13820        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13821        let mut grouping_universe: Vec<Expr> = Vec::new();
13822        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13823        // A BOOL, not the key list: this frame is the statement parser's, and
13824        // round 430 measured that a `Vec` local here is enough on its own to
13825        // tip the 512 KiB nesting guard. The keys are recoverable from
13826        // `grouping_universe`, which a rollup fills with exactly them.
13827        let mut mysql_rollup = false;
13828        let group_by = if matches!(self.peek(), Token::Group) {
13829            self.advance();
13830            if !self.peek_is_by() {
13831                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13832            }
13833            self.advance();
13834            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13835            // every non-aggregate SELECT-list item later.
13836            if matches!(self.peek(), Token::All) {
13837                self.advance();
13838                group_by_all = true;
13839                None
13840            } else {
13841                // v7.39 (round 242) — PG's general grouping-element grammar:
13842                // GROUP BY [DISTINCT] element [, element]*, where an element
13843                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13844                // SETS (…) — mixed freely. Each element yields a list of
13845                // key sets; the query's grouping sets are the CARTESIAN
13846                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13847                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13848                // content. ROLLUP/CUBE members may be composite
13849                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13850                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13851                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13852                // clause.
13853                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13854                    self.advance();
13855                    true
13856                } else {
13857                    false
13858                };
13859                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13860                loop {
13861                    element_sets.push(self.parse_grouping_element()?);
13862                    if matches!(self.peek(), Token::Comma) {
13863                        self.advance();
13864                    } else {
13865                        break;
13866                    }
13867                }
13868                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13869                for el in &element_sets {
13870                    let mut next: Vec<Vec<Expr>> = Vec::new();
13871                    for base in &total {
13872                        for set in el {
13873                            let mut merged = base.clone();
13874                            for k in set {
13875                                if !merged.iter().any(|m| m == k) {
13876                                    merged.push(k.clone());
13877                                }
13878                            }
13879                            next.push(merged);
13880                        }
13881                    }
13882                    total = next;
13883                }
13884                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13885                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13886                // The keys and the aggregates come out identical; the ROW
13887                // ORDER does not, and that is the part a report depends on.
13888                // MySQL interleaves each group's subtotal right after its
13889                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13890                // where the union-of-grouping-sets expansion emits every
13891                // leaf first and then every subtotal. MariaDB REFUSES an
13892                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13893                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13894                // agree on the order and disagree only on whether ORDER BY
13895                // is allowed (MySQL allows it; SPG allows it too, since
13896                // refusing would break the clients that can write it).
13897                if self.mysql_dialect
13898                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13899                    && matches!(
13900                        self.tokens.get(self.pos + 1),
13901                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13902                    )
13903                {
13904                    self.advance(); // WITH
13905                    self.advance(); // ROLLUP
13906                    let keys = total.into_iter().next().unwrap_or_default();
13907                    mysql_rollup = true;
13908                    // n+1 prefixes, largest first — the same expansion
13909                    // `ROLLUP (…)` produces.
13910                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13911                }
13912                if distinct_sets {
13913                    let mut seen: Vec<Vec<String>> = Vec::new();
13914                    total.retain(|set| {
13915                        let mut key: Vec<String> =
13916                            set.iter().map(|e| alloc::format!("{e}")).collect();
13917                        key.sort();
13918                        if seen.contains(&key) {
13919                            false
13920                        } else {
13921                            seen.push(key);
13922                            true
13923                        }
13924                    });
13925                }
13926                if total.len() > 1 {
13927                    let mut universe: Vec<Expr> = Vec::new();
13928                    for set in &total {
13929                        for k in set {
13930                            if !universe.iter().any(|u| u == k) {
13931                                universe.push(k.clone());
13932                            }
13933                        }
13934                    }
13935                    grouping_universe = universe;
13936                    let primary = total[0].clone();
13937                    grouping_sets = total;
13938                    Some(primary)
13939                } else {
13940                    // One set (a plain GROUP BY list, or a single-set
13941                    // spelling like GROUPING SETS ((a, b))). An EMPTY
13942                    // single set — GROUPING SETS (()) — stays
13943                    // `Some(vec![])`: the grand-total group, which must
13944                    // run the aggregate path.
13945                    Some(total.into_iter().next().unwrap_or_default())
13946                }
13947            }
13948        } else {
13949            None
13950        };
13951        let having = if matches!(self.peek(), Token::Having) {
13952            self.advance();
13953            Some(self.parse_expr(0)?)
13954        } else {
13955            None
13956        };
13957        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13958        // OVER w parsed to a marker above; inline each definition
13959        // into the referencing WindowFunction nodes.
13960        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13961        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13962            self.advance();
13963            loop {
13964                let wname = self.expect_ident_like()?;
13965                if !matches!(self.peek(), Token::As) {
13966                    return Err(self.err(format!(
13967                        "expected AS after WINDOW {wname}, got {:?}",
13968                        self.peek()
13969                    )));
13970                }
13971                self.advance();
13972                // v7.39 (round 229) — PG rejects a redefinition outright.
13973                if window_defs
13974                    .iter()
13975                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13976                {
13977                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13978                }
13979                let def = self.parse_over_clause()?;
13980                // A definition may itself copy an earlier one
13981                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13982                // so resolve it against the defs already in scope. Same
13983                // copy rules as an `OVER (w1 …)` in the select list.
13984                let mut probe = Expr::WindowFunction {
13985                    name: String::new(),
13986                    args: Vec::new(),
13987                    partition_by: def.0,
13988                    order_by: def.1,
13989                    frame: def.2,
13990                    null_treatment: crate::ast::NullTreatment::Respect,
13991                    filter: None,
13992                };
13993                Self::substitute_named_windows(&mut probe, &window_defs)
13994                    .map_err(|m| self.err(m))?;
13995                let Expr::WindowFunction {
13996                    partition_by,
13997                    order_by,
13998                    frame,
13999                    ..
14000                } = probe
14001                else {
14002                    unreachable!("probe is a WindowFunction")
14003                };
14004                window_defs.push((wname, (partition_by, order_by, frame)));
14005                if matches!(self.peek(), Token::Comma) {
14006                    self.advance();
14007                    continue;
14008                }
14009                break;
14010            }
14011        }
14012        // v7.39 (round 705) — which definitions did anything reference?
14013        // The ones nothing did used to be dropped here, unexamined, so
14014        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14015        // definition whether referenced or not. Their key expressions ride
14016        // out on the statement for the engine to resolve.
14017        let mut window_refs: Vec<String> = Vec::new();
14018        if !window_defs.is_empty() {
14019            for it in &items {
14020                if let SelectItem::Expr { expr, .. } = it {
14021                    Self::collect_named_window_refs(expr, &mut window_refs);
14022                }
14023            }
14024        }
14025        let window_check_exprs: Vec<Expr> = window_defs
14026            .iter()
14027            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14028            .flat_map(|(_, (partition, order, _))| {
14029                partition
14030                    .iter()
14031                    .cloned()
14032                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14033            })
14034            .collect();
14035        if !window_defs.is_empty()
14036            || items
14037                .iter()
14038                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14039        {
14040            for it in &mut items {
14041                if let SelectItem::Expr { expr, .. } = it {
14042                    Self::substitute_named_windows(expr, &window_defs)
14043                        .map_err(|m| self.err(m))?;
14044                }
14045            }
14046        }
14047        // `GROUP BY 1` — positional keys substitute with the Nth
14048        // select item's expression (same contract ORDER BY has had
14049        // since v6.x). Out-of-range positions error.
14050        let group_by = match group_by {
14051            Some(mut keys) => {
14052                for k in &mut keys {
14053                    if let Expr::Literal(Literal::Integer(n)) = k {
14054                        let idx = *n;
14055                        if idx < 1 || idx as usize > items.len() {
14056                            return Err(self.err(alloc::format!(
14057                                "GROUP BY position {idx} is not in select list"
14058                            )));
14059                        }
14060                        match &items[(idx - 1) as usize] {
14061                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14062                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14063                                return Err(self.err(alloc::format!(
14064                                    "GROUP BY position {idx} references a wildcard item"
14065                                )));
14066                            }
14067                        }
14068                    }
14069                }
14070                Some(keys)
14071            }
14072            None => None,
14073        };
14074        let mut stmt = SelectStatement {
14075            locking: None,
14076            ctes: Vec::new(),
14077            distinct,
14078            distinct_on,
14079            items,
14080            from,
14081            where_,
14082            group_by,
14083            group_by_all,
14084            having,
14085            unions: Vec::new(),
14086            order_by: Vec::new(),
14087            limit: None,
14088            offset: None,
14089            limit_with_ties: false,
14090            window_check_exprs,
14091        };
14092        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14093        // first set is the primary (already on stmt.group_by); each
14094        // further set becomes a UNION ALL peer with its dropped
14095        // keys (universe minus the set) replaced by NULL literals
14096        // in the peer's items and group_by. PG-legal: non-grouped
14097        // select items must be group keys or aggregates, so a
14098        // dropped key's occurrences in the projection are exactly
14099        // the ones to nullify.
14100        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14101        // over a plain GROUP BY (every argument must be a group key; the
14102        // mask is then 0) and rejects anything else with 42803. SPG's
14103        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14104        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14105        // function `grouping`".
14106        if grouping_sets.len() <= 1 {
14107            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14108            let mut calls: Vec<Expr> = Vec::new();
14109            for item in &stmt.items {
14110                if let SelectItem::Expr { expr, .. } = item {
14111                    Self::collect_grouping_calls(expr, &mut calls);
14112                }
14113            }
14114            if let Some(h) = &stmt.having {
14115                Self::collect_grouping_calls(h, &mut calls);
14116            }
14117            for call in &calls {
14118                let Expr::FunctionCall { args, .. } = call else {
14119                    continue;
14120                };
14121                for a in args {
14122                    if !keys.iter().any(|k| k == a) {
14123                        return Err(self.err(
14124                            "arguments to GROUPING must be grouping expressions of the associated query level"
14125                                .to_string(),
14126                        ));
14127                    }
14128                }
14129            }
14130            if !calls.is_empty() {
14131                for item in &mut stmt.items {
14132                    if let SelectItem::Expr { expr, .. } = item {
14133                        Self::substitute_grouping_calls(expr, &[]);
14134                    }
14135                }
14136                if let Some(h) = &mut stmt.having {
14137                    Self::substitute_grouping_calls(h, &[]);
14138                }
14139            }
14140        }
14141        if grouping_sets.len() > 1 {
14142            // The primary set's own dropped keys nullify in the
14143            // HEAD's projection too (GROUPING SETS's first set may
14144            // omit keys other sets use).
14145            let primary = grouping_sets[0].clone();
14146            let head_dropped: Vec<Expr> = grouping_universe
14147                .iter()
14148                .filter(|u| !primary.iter().any(|k| k == *u))
14149                .cloned()
14150                .collect();
14151            for set in grouping_sets.iter().skip(1) {
14152                let mut peer = stmt.clone();
14153                peer.unions = Vec::new();
14154                let dropped: Vec<&Expr> = grouping_universe
14155                    .iter()
14156                    .filter(|u| !set.iter().any(|k| k == *u))
14157                    .collect();
14158                // Empty set = grand-total group: `Some(vec![])` forces
14159                // the aggregate path (one collapsed row) instead of a
14160                // per-row passthrough. See the primary-set note above.
14161                peer.group_by = Some(set.clone());
14162                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14163                for item in &mut peer.items {
14164                    if let SelectItem::Expr { expr, alias } = item {
14165                        if dropped.iter().any(|d| *d == expr) {
14166                            // v7.39 — keep the dropped key's name on the
14167                            // NULL literal so the UNION output column
14168                            // (and any top-level ORDER BY on it) still
14169                            // resolves.
14170                            if alias.is_none()
14171                                && let Expr::Column(c) = &expr
14172                            {
14173                                *alias = Some(c.name.clone());
14174                            }
14175                            *expr = Expr::Literal(Literal::Null);
14176                        } else {
14177                            Self::substitute_grouping_calls(expr, &dropped_owned);
14178                        }
14179                    }
14180                }
14181                if let Some(h) = &mut peer.having {
14182                    Self::substitute_grouping_calls(h, &dropped_owned);
14183                }
14184                stmt.unions.push((UnionKind::All, peer));
14185            }
14186            for item in &mut stmt.items {
14187                if let SelectItem::Expr { expr, alias } = item {
14188                    if head_dropped.iter().any(|d| d == expr) {
14189                        if alias.is_none()
14190                            && let Expr::Column(c) = &expr
14191                        {
14192                            *alias = Some(c.name.clone());
14193                        }
14194                        *expr = Expr::Literal(Literal::Null);
14195                    } else {
14196                        Self::substitute_grouping_calls(expr, &head_dropped);
14197                    }
14198                }
14199            }
14200            if let Some(h) = &mut stmt.having {
14201                Self::substitute_grouping_calls(h, &head_dropped);
14202            }
14203            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14204            // (while `grouping_universe` / the per-branch sets are in scope). For
14205            // each grouping() call in it, inject a per-branch hidden column
14206            // `__grp_ord_K` carrying that branch's mask into the head + every
14207            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14208            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14209            // from the final output. A standalone grouping-set query has ORDER BY
14210            // (not an explicit set-op) next, so consuming it here is safe.
14211            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14212            // rollup carries the hierarchical order: sort by the grouping
14213            // keys with the rolled-up NULLs last, which is exactly the
14214            // interleaving both oracles emit. A client's own ORDER BY wins,
14215            // which is what MySQL does (MariaDB refuses to let one be
14216            // written at all).
14217            // The synthesised keys have to travel the SAME path a written
14218            // ORDER BY does: the block below is what turns a `grouping()`
14219            // call into the per-branch `__grp_ord_K` column the engine can
14220            // actually sort on. Bypassing it left a bare `grouping(text)`
14221            // for the evaluator to reject.
14222            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14223                self.parse_order_by_keys()?
14224            } else if mysql_rollup {
14225                Self::mysql_rollup_order(&grouping_universe)
14226            } else {
14227                Vec::new()
14228            };
14229            if !synthesised_or_parsed.is_empty() {
14230                let mut order_keys = synthesised_or_parsed;
14231                let mut grp_exprs: Vec<Expr> = Vec::new();
14232                for ob in &order_keys {
14233                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14234                }
14235                for (k, gexpr) in grp_exprs.iter().enumerate() {
14236                    let colname = alloc::format!("__grp_ord_{k}");
14237                    // Head branch (primary set) uses `head_dropped`.
14238                    let mut he = gexpr.clone();
14239                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14240                    stmt.items.push(SelectItem::Expr {
14241                        expr: he,
14242                        alias: Some(colname.clone()),
14243                    });
14244                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14245                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14246                        let set = &grouping_sets[i + 1];
14247                        let dropped: Vec<Expr> = grouping_universe
14248                            .iter()
14249                            .filter(|u| !set.iter().any(|k| k == *u))
14250                            .cloned()
14251                            .collect();
14252                        let mut pe = gexpr.clone();
14253                        Self::substitute_grouping_calls(&mut pe, &dropped);
14254                        peer.items.push(SelectItem::Expr {
14255                            expr: pe,
14256                            alias: Some(colname.clone()),
14257                        });
14258                    }
14259                }
14260                for ob in &mut order_keys {
14261                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14262                }
14263                stmt.order_by = order_keys;
14264            }
14265        }
14266        Ok(stmt)
14267    }
14268
14269    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14270    /// as ORDER BY keys.
14271    ///
14272    /// Per key: the rollup marker, then the key. Sorting on the key alone
14273    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14274    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14275    /// the ROLLUP-introduced NULL last, and both print as NULL.
14276    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14277    /// real group including the data-NULL one, 1 only for the row the
14278    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14279    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14280    ///
14281    /// `#[inline(never)]`: its locals must not join the statement parser's
14282    /// frame, which round 430 measured sitting against the nesting guard.
14283    #[inline(never)]
14284    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14285        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14286        for e in keys {
14287            out.push(OrderBy {
14288                expr: Expr::FunctionCall {
14289                    name: "grouping".into(),
14290                    args: alloc::vec![e.clone()],
14291                },
14292                desc: false,
14293                nulls_first: None,
14294                collation: None,
14295            });
14296            out.push(OrderBy {
14297                expr: e.clone(),
14298                desc: false,
14299                // MySQL orders NULL first on an ascending key.
14300                nulls_first: Some(true),
14301                collation: None,
14302            });
14303        }
14304        out
14305    }
14306
14307    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14308    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14309    #[inline(never)]
14310    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14311        use crate::ast::MaintainKind;
14312        self.skip_paren_option_list();
14313        let kind = match self.peek() {
14314            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14315            Token::Table | Token::Index => {
14316                self.advance();
14317                MaintainKind::ReindexRelation
14318            }
14319            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14320                "index" | "table" => {
14321                    self.advance();
14322                    MaintainKind::ReindexRelation
14323                }
14324                "schema" => {
14325                    self.advance();
14326                    MaintainKind::ReindexSchema
14327                }
14328                "system" | "database" => {
14329                    self.advance();
14330                    MaintainKind::Whole
14331                }
14332                // PG requires the object type; anything else is the
14333                // caller's problem, not something to swallow.
14334                _ => MaintainKind::ReindexRelation,
14335            },
14336            _ => MaintainKind::Whole,
14337        };
14338        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14339        // allows the plain form, so the modifier is recorded rather than
14340        // skipped. It still has no effect on how the reindex runs.
14341        let mut concurrently = false;
14342        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14343            self.advance();
14344            concurrently = true;
14345        }
14346        let target = self.take_optional_maintain_name();
14347        self.consume_until_statement_boundary();
14348        Ok(Statement::Maintain {
14349            kind,
14350            concurrently,
14351            target,
14352        })
14353    }
14354
14355    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14356    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14357    #[inline(never)]
14358    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14359        use crate::ast::MaintainKind;
14360        self.skip_paren_option_list();
14361        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14362            self.advance();
14363        }
14364        let target = self.take_optional_maintain_name();
14365        self.consume_until_statement_boundary();
14366        Ok(Statement::Maintain {
14367            kind: if target.is_some() {
14368                MaintainKind::ClusterRelation
14369            } else {
14370                MaintainKind::Whole
14371            },
14372            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14373            // transaction block quite happily (measured).
14374            concurrently: false,
14375            target,
14376        })
14377    }
14378
14379    /// The next token as a relation / schema name, when there is one.
14380    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14381        match self.peek() {
14382            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14383                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14384                _ => None,
14385            },
14386            _ => None,
14387        }
14388    }
14389
14390    /// A parenthesised option list, absorbed.
14391    fn skip_paren_option_list(&mut self) {
14392        if !matches!(self.peek(), Token::LParen) {
14393            return;
14394        }
14395        let mut depth = 0usize;
14396        loop {
14397            match self.advance() {
14398                Token::LParen => depth += 1,
14399                Token::RParen => {
14400                    depth -= 1;
14401                    if depth == 0 {
14402                        return;
14403                    }
14404                }
14405                Token::Eof => return,
14406                _ => {}
14407            }
14408        }
14409    }
14410
14411    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14412    /// column list.
14413    ///
14414    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14415    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14416    /// / ALL. The three that describe physical storage have no meaning
14417    /// here, so they parse and change nothing rather than making a
14418    /// dump that mentions them fail to load.
14419    ///
14420    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14421    /// parse chain the nesting sentinel is tuned against.
14422    #[inline(never)]
14423    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14424        self.advance(); // LIKE
14425        let source = self.expect_ident_like()?;
14426        let mut options = crate::ast::LikeOptions::default();
14427        loop {
14428            let including = match self.peek() {
14429                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14430                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14431                _ => break,
14432            };
14433            self.advance();
14434            // `ALL` lexes as its own keyword, not an identifier.
14435            let opt = if matches!(self.peek(), Token::All) {
14436                self.advance();
14437                alloc::string::String::from("all")
14438            } else {
14439                self.expect_ident_like()?
14440            };
14441            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14442                o.defaults = on;
14443                o.constraints = on;
14444                o.identity = on;
14445                o.generated = on;
14446                o.indexes = on;
14447                o.comments = on;
14448            };
14449            match opt.to_ascii_lowercase().as_str() {
14450                "all" => set(&mut options, including),
14451                "defaults" => options.defaults = including,
14452                "constraints" => options.constraints = including,
14453                "identity" => options.identity = including,
14454                "generated" => options.generated = including,
14455                "indexes" => options.indexes = including,
14456                "comments" => options.comments = including,
14457                // No storage model to copy into.
14458                "storage" | "statistics" | "compression" => {}
14459                other => {
14460                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14461                }
14462            }
14463        }
14464        Ok(crate::ast::LikeSpec {
14465            source,
14466            at,
14467            options,
14468        })
14469    }
14470
14471    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14472        // Caller already consumed CREATE; we're sitting on TABLE.
14473        debug_assert!(matches!(self.peek(), Token::Table));
14474        self.advance();
14475        let if_not_exists = self.consume_if_not_exists();
14476        let name = self.expect_ident_like()?;
14477        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14478        // child shape has no column list; the child inherits its
14479        // columns from the parent at engine-DDL time. Detect it
14480        // before the `(` requirement below.
14481        if matches!(self.peek(), Token::Partition)
14482            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14483        {
14484            self.advance(); // PARTITION
14485            self.advance(); // of
14486            let partition_of = self.parse_partition_of_tail()?;
14487            return Ok(Statement::CreateTable(CreateTableStatement {
14488                temporary: false,
14489                name,
14490                engine: None,
14491                columns: Vec::new(),
14492                like_specs: Vec::new(),
14493                inherits: Vec::new(),
14494                if_not_exists,
14495                foreign_keys: Vec::new(),
14496                table_constraints: Vec::new(),
14497                partition_by: None,
14498                partition_of: Some(partition_of),
14499            }));
14500        }
14501        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14502        // the materialized-view materialisation path (run the SELECT, infer the
14503        // column types, create + populate the table) but marks the node so the
14504        // executor creates a plain table without a mat-view registry entry.
14505        if matches!(self.peek(), Token::As) {
14506            self.advance();
14507            let body_stmt = self.parse_select_stmt()?;
14508            let Statement::Select(body) = body_stmt else {
14509                return Err(self.err(format!(
14510                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14511                )));
14512            };
14513            let with_data = self.parse_optional_with_data(true)?;
14514            return Ok(Statement::CreateMaterializedView(
14515                crate::ast::CreateMaterializedViewStatement {
14516                    temporary: false,
14517                    name,
14518                    if_not_exists,
14519                    columns: Vec::new(),
14520                    body,
14521                    with_data,
14522                    as_plain_table: true,
14523                },
14524            ));
14525        }
14526        if !matches!(self.peek(), Token::LParen) {
14527            return Err(self.err(format!(
14528                "expected '(' after table name, got {:?}",
14529                self.peek()
14530            )));
14531        }
14532        self.advance();
14533        let mut columns = Vec::new();
14534        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14535        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14536        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14537        loop {
14538            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14539            // column list. It is how a child that adds nothing of its own is
14540            // written, and this loop demanded at least one entry: `syntax
14541            // error at or near ")"`. The child takes the parent's columns,
14542            // which the INHERITS clause already arranges.
14543            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14544                self.advance();
14545                break;
14546            }
14547            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14548            // clauses from column definitions. Constraints start
14549            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14550            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14551            // a column.
14552            if self.peek_table_level_pk_start() {
14553                table_constraints.push(self.parse_table_level_primary_key()?);
14554            } else if matches!(self.peek(), Token::Like) {
14555                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14556                // <opt> ]*`. The source table's shape lives in the catalog,
14557                // so this records the clause and the engine expands it.
14558                like_specs.push(self.parse_create_table_like(columns.len())?);
14559            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14560                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14561                table_constraints.push(self.parse_table_level_exclude()?);
14562            } else if self.peek_table_level_unique_start() {
14563                table_constraints.push(self.parse_table_level_unique()?);
14564            } else if self.peek_table_level_check_start() {
14565                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14566                table_constraints.push(self.parse_table_level_check()?);
14567            } else if self.peek_mysql_inline_key_start() {
14568                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14569                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14570                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14571                // inside the column list. Skip name + paren list;
14572                // for UNIQUE KEY, register as a UC.
14573                if let Some(uc) = self.parse_mysql_inline_key()? {
14574                    table_constraints.push(uc);
14575                }
14576            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14577                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14578                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14579                // CHECK is named, and the named-CONSTRAINT arm used
14580                // to accept FOREIGN KEY only. The name is accepted
14581                // and discarded — same handling as every other SPG
14582                // constraint name.
14583                self.advance(); // CONSTRAINT
14584                // v7.39 (read01 round 48) — the name is kept now: the schema
14585                // stores it, so DROP / RENAME CONSTRAINT can find it.
14586                let con_name = self.expect_ident_like()?;
14587                let mut tc = match kind {
14588                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14589                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14590                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14591                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14592                };
14593                match &mut tc {
14594                    crate::ast::TableConstraint::Check { name, .. }
14595                    | crate::ast::TableConstraint::Unique { name, .. }
14596                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14597                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14598                        *name = Some(con_name);
14599                    }
14600                    _ => {}
14601                }
14602                table_constraints.push(tc);
14603            } else if self.peek_constraint_or_fk_start() {
14604                foreign_keys.push(self.parse_table_level_fk()?);
14605            } else {
14606                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14607                // v7.13.0 — fold inline UNIQUE / CHECK column
14608                // constraints into table-level entries so the
14609                // engine path stays uniform.
14610                if col.is_unique {
14611                    table_constraints.push(crate::ast::TableConstraint::Unique {
14612                        name: None,
14613                        columns: alloc::vec![col.name.clone()],
14614                        nulls_not_distinct: col.unique_nulls_not_distinct,
14615                        deferrable: col.constraint_deferrable,
14616                        initially_deferred: col.constraint_initially_deferred,
14617                    });
14618                }
14619                if let Some(check_expr) = col.check.clone() {
14620                    table_constraints.push(crate::ast::TableConstraint::Check {
14621                        name: None,
14622                        expr: check_expr,
14623                        not_valid: false,
14624                    });
14625                }
14626                columns.push(col);
14627                if let Some(fk) = col_level_fk {
14628                    foreign_keys.push(fk);
14629                }
14630            }
14631            match self.peek() {
14632                Token::Comma => {
14633                    self.advance();
14634                }
14635                Token::RParen => {
14636                    self.advance();
14637                    break;
14638                }
14639                other => {
14640                    return Err(
14641                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14642                    );
14643                }
14644            }
14645        }
14646        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14647        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14648        // nothing is written between the parentheses.
14649        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14650        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14651        // empty parentheses were a parse error in their own right — quite apart
14652        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14653        // SPG does not have (filed separately).
14654        let _ = &like_specs;
14655        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14656        // It sits between the column list and the MySQL table options,
14657        // and it was a syntax error until this round.
14658        let mut inherits: Vec<String> = Vec::new();
14659        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14660            if k.eq_ignore_ascii_case("inherits"))
14661        {
14662            self.advance();
14663            if !matches!(self.peek(), Token::LParen) {
14664                return Err(self.err(alloc::format!(
14665                    "expected ( after INHERITS, got {:?}",
14666                    self.peek()
14667                )));
14668            }
14669            self.advance();
14670            loop {
14671                inherits.push(self.expect_ident_like()?);
14672                if matches!(self.peek(), Token::Comma) {
14673                    self.advance();
14674                    continue;
14675                }
14676                break;
14677            }
14678            if !matches!(self.peek(), Token::RParen) {
14679                return Err(self.err(alloc::format!(
14680                    "expected ) closing INHERITS, got {:?}",
14681                    self.peek()
14682                )));
14683            }
14684            self.advance();
14685        }
14686        // v7.14.0 — consume MySQL/MariaDB table options after the
14687        // closing `)`. mysqldump emits things like
14688        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14689        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14690        // SPG accepts all forms as no-ops (each option is
14691        // `<ident> [=] <ident-or-string>` separated by whitespace).
14692        let engine = self.consume_mysql_table_options();
14693        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14694        // SPG has no per-table reloptions, so accept and ignore them so a
14695        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14696        self.consume_with_reloptions();
14697        // v7.37.6-B — declarative-partition-parent suffix
14698        // (`PARTITION BY RANGE (key_col)`) sits after the column
14699        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14700        // and locks the key column at one ident; the engine then
14701        // verifies the column type is TIMESTAMPTZ.
14702        let partition_by = if matches!(self.peek(), Token::Partition) {
14703            self.advance(); // PARTITION
14704            if !self.peek_is_by() {
14705                return Err(self.err(format!(
14706                    "expected BY after PARTITION, got {:?}",
14707                    self.peek()
14708                )));
14709            }
14710            self.advance();
14711            Some(self.parse_partition_by_tail()?)
14712        } else {
14713            None
14714        };
14715        Ok(Statement::CreateTable(CreateTableStatement {
14716            temporary: false,
14717            name,
14718            engine,
14719            columns,
14720            like_specs,
14721            inherits,
14722            if_not_exists,
14723            foreign_keys,
14724            table_constraints,
14725            partition_by,
14726            partition_of: None,
14727        }))
14728    }
14729
14730    /// v7.37.6-B — case-insensitive ident match helper for the
14731    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14732    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14733    /// didn't burn a global keyword slot for each (see the
14734    /// `Token::Partition` doc-comment in `lexer.rs`).
14735    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14736        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14737    }
14738
14739    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14740    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14741    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14742        use crate::ast::{PartitionBySpec, PartitionKindAst};
14743        let kind = match self.peek() {
14744            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14745                self.advance();
14746                PartitionKindAst::Range
14747            }
14748            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14749                self.advance();
14750                PartitionKindAst::List
14751            }
14752            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14753                self.advance();
14754                PartitionKindAst::Hash
14755            }
14756            other => {
14757                return Err(self.err(format!(
14758                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14759                )));
14760            }
14761        };
14762        if !matches!(self.peek(), Token::LParen) {
14763            return Err(self.err(format!(
14764                "expected '(' after PARTITION BY <strategy>, got {:?}",
14765                self.peek()
14766            )));
14767        }
14768        self.advance();
14769        let mut key_columns = Vec::new();
14770        loop {
14771            key_columns.push(self.expect_ident_like()?);
14772            match self.peek() {
14773                Token::Comma => {
14774                    self.advance();
14775                }
14776                Token::RParen => {
14777                    self.advance();
14778                    break;
14779                }
14780                other => {
14781                    return Err(self.err(format!(
14782                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14783                    )));
14784                }
14785            }
14786        }
14787        if key_columns.is_empty() {
14788            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14789        }
14790        Ok(PartitionBySpec { kind, key_columns })
14791    }
14792
14793    /// v7.37.6-B — after `PARTITION OF`, expect
14794    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14795    /// or
14796    ///   <parent> DEFAULT
14797    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14798        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14799        let parent_name = self.expect_ident_like()?;
14800        // v7.37.6-B rejects an explicit column list — the child
14801        // inherits from the parent. mailrs round-7 taught us that
14802        // CREATE TABLE-side schema reconciliation hides drift, so
14803        // we surface this as a parse error rather than silently
14804        // ignoring user columns.
14805        if matches!(self.peek(), Token::LParen) {
14806            return Err(self.err(
14807                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14808                 at v7.37.6-B; the child inherits its columns from the parent"
14809                    .to_string(),
14810            ));
14811        }
14812        let bounds = match self.peek() {
14813            Token::Default => {
14814                self.advance();
14815                PartitionOfBoundsAst::Default
14816            }
14817            Token::For => {
14818                self.advance();
14819                if !matches!(self.peek(), Token::Values) {
14820                    return Err(
14821                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14822                    );
14823                }
14824                self.advance();
14825                // WITH is not a reserved Token in the lexer — it lexes
14826                // as Token::Ident("with"). Disambiguate manually.
14827                let want_with = matches!(
14828                    self.peek(),
14829                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14830                );
14831                if want_with {
14832                    self.advance();
14833                    if !matches!(self.peek(), Token::LParen) {
14834                        return Err(self.err(format!(
14835                            "expected '(' after FOR VALUES WITH, got {:?}",
14836                            self.peek()
14837                        )));
14838                    }
14839                    self.advance();
14840                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14841                    loop {
14842                        let key = self.expect_ident_like()?;
14843                        let n = match self.peek().clone() {
14844                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14845                                self.advance();
14846                                v as u32
14847                            }
14848                            other => {
14849                                return Err(self.err(format!(
14850                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14851                                )));
14852                            }
14853                        };
14854                        match key.to_ascii_uppercase().as_str() {
14855                            "MODULUS" => modulus = Some(n),
14856                            "REMAINDER" => remainder = Some(n),
14857                            other => {
14858                                return Err(self.err(format!(
14859                                    "FOR VALUES WITH: unknown key {other:?}; \
14860                                     expected MODULUS or REMAINDER"
14861                                )));
14862                            }
14863                        }
14864                        match self.peek() {
14865                            Token::Comma => {
14866                                self.advance();
14867                            }
14868                            Token::RParen => {
14869                                self.advance();
14870                                break;
14871                            }
14872                            other => {
14873                                return Err(self.err(format!(
14874                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14875                                )));
14876                            }
14877                        }
14878                    }
14879                    let modulus = modulus
14880                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14881                    let remainder = remainder.ok_or_else(|| {
14882                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14883                    })?;
14884                    if modulus == 0 {
14885                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14886                    }
14887                    if remainder >= modulus {
14888                        return Err(self.err(format!(
14889                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14890                             must be < MODULUS ({modulus})"
14891                        )));
14892                    }
14893                    PartitionOfBoundsAst::Hash { modulus, remainder }
14894                } else {
14895                    match self.peek() {
14896                        Token::From => {
14897                            self.advance();
14898                            let lower = Box::new(self.parse_partition_bound_expr()?);
14899                            if !matches!(self.peek(), Token::To) {
14900                                return Err(self.err(format!(
14901                                    "expected TO after FROM (...), got {:?}",
14902                                    self.peek()
14903                                )));
14904                            }
14905                            self.advance();
14906                            let upper = Box::new(self.parse_partition_bound_expr()?);
14907                            PartitionOfBoundsAst::Range { lower, upper }
14908                        }
14909                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14910                        Token::In => {
14911                            self.advance();
14912                            if !matches!(self.peek(), Token::LParen) {
14913                                return Err(self.err(format!(
14914                                    "expected '(' after FOR VALUES IN, got {:?}",
14915                                    self.peek()
14916                                )));
14917                            }
14918                            self.advance();
14919                            let mut values = Vec::new();
14920                            loop {
14921                                values.push(self.parse_expr(0)?);
14922                                match self.peek() {
14923                                    Token::Comma => {
14924                                        self.advance();
14925                                    }
14926                                    Token::RParen => {
14927                                        self.advance();
14928                                        break;
14929                                    }
14930                                    other => {
14931                                        return Err(self.err(format!(
14932                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14933                                    )));
14934                                    }
14935                                }
14936                            }
14937                            if values.is_empty() {
14938                                return Err(self.err(
14939                                    "FOR VALUES IN requires at least one literal".to_string(),
14940                                ));
14941                            }
14942                            PartitionOfBoundsAst::List { values }
14943                        }
14944                        other => {
14945                            return Err(self.err(format!(
14946                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14947                            )));
14948                        }
14949                    }
14950                }
14951            }
14952            other => {
14953                return Err(self.err(format!(
14954                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14955                )));
14956            }
14957        };
14958        Ok(PartitionOfSpec {
14959            parent_name,
14960            bounds,
14961        })
14962    }
14963
14964    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14965    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14966    /// markers (no-arg builtins) so the engine resolves them
14967    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14968    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14969        if !matches!(self.peek(), Token::LParen) {
14970            return Err(self.err(format!(
14971                "expected '(' before partition bound, got {:?}",
14972                self.peek()
14973            )));
14974        }
14975        self.advance();
14976        let expr = match self.peek() {
14977            Token::Ident(s) | Token::QuotedIdent(s)
14978                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14979            {
14980                let name = s.to_ascii_uppercase();
14981                self.advance();
14982                crate::ast::Expr::FunctionCall {
14983                    name,
14984                    args: Vec::new(),
14985                }
14986            }
14987            _ => self.parse_expr(0)?,
14988        };
14989        if !matches!(self.peek(), Token::RParen) {
14990            return Err(self.err(format!(
14991                "expected ')' after partition bound, got {:?}",
14992                self.peek()
14993            )));
14994        }
14995        self.advance();
14996        Ok(expr)
14997    }
14998
14999    /// v7.14.0 — true when the next tokens look like an inline
15000    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15001    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15002    /// — each followed by an optional name + `(...)`. Critical:
15003    /// a column NAMED `key` / `index` (PG accepts as ident) must
15004    /// NOT be mistaken for the KEY constraint shape. We disambig
15005    /// by requiring the keyword to be followed by either `(` or
15006    /// `<ident> (`.
15007    fn peek_mysql_inline_key_start(&self) -> bool {
15008        let cur = self.peek();
15009        // Shapes:
15010        //   KEY (cols)
15011        //   KEY name (cols)
15012        //   INDEX (cols)
15013        //   INDEX name (cols)
15014        //   UNIQUE KEY [name] (cols)
15015        //   UNIQUE INDEX [name] (cols)
15016        //   FULLTEXT [KEY|INDEX] [name] (cols)
15017        //   SPATIAL [KEY|INDEX] [name] (cols)
15018        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15019            // tokens at skip = the position AFTER the index-form
15020            // keywords (KEY/INDEX) have been consumed.
15021            match self.tokens.get(skip) {
15022                Some(Token::LParen) => true,
15023                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15024                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15025                }
15026                _ => false,
15027            }
15028        };
15029        // `INDEX` lexes as Token::Index (reserved), not as
15030        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15031        // start; the peek helper below handles either.
15032        let is_key_or_index_tok = |t: &Token| -> bool {
15033            matches!(t, Token::Index)
15034                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15035        };
15036        match cur {
15037            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15038            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15039                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15040            }
15041            Token::Ident(s)
15042                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15043            {
15044                let nxt = self.tokens.get(self.pos + 1);
15045                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15046                    self.pos + 2
15047                } else {
15048                    self.pos + 1
15049                };
15050                after_keyword_followed_by_paren_or_ident_paren(after_after)
15051            }
15052            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15053                let nxt = self.tokens.get(self.pos + 1);
15054                if !nxt.is_some_and(is_key_or_index_tok) {
15055                    return false;
15056                }
15057                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15058            }
15059            _ => false,
15060        }
15061    }
15062
15063    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15064    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15065    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15066    /// returns Some(TableConstraint::Index) so the engine builds
15067    /// a real BTree index on the leading column (mysqldump
15068    /// `KEY idx_posts_author (author_id)` shape).
15069    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15070    /// (the storage layer has no matching AM).
15071    fn parse_mysql_inline_key(
15072        &mut self,
15073    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15074        // Detect UNIQUE prefix.
15075        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15076        {
15077            self.advance();
15078            true
15079        } else {
15080            false
15081        };
15082        // Consume FULLTEXT / SPATIAL prefix and record which one
15083        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15084        // dedicated TableConstraint variant so the engine can
15085        // build a tsvector-GIN; SPATIAL still has no matching
15086        // AM, so it falls back to accept-as-no-op.
15087        let mut is_fulltext = false;
15088        let mut is_spatial = false;
15089        if let Token::Ident(s) = self.peek().clone() {
15090            if s.eq_ignore_ascii_case("fulltext") {
15091                self.advance();
15092                is_fulltext = true;
15093            } else if s.eq_ignore_ascii_case("spatial") {
15094                self.advance();
15095                is_spatial = true;
15096            }
15097        }
15098        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15099        // (reserved); accept either token shape.
15100        match self.peek() {
15101            Token::Index => {
15102                self.advance();
15103            }
15104            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15105                self.advance();
15106            }
15107            other => {
15108                return Err(self.err(alloc::format!(
15109                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15110                )));
15111            }
15112        }
15113        // Optional index name (an ident before the `(`).
15114        // v7.15.0 — capture the name when present so the engine
15115        // builds the secondary index under the user's chosen
15116        // name (matches mysqldump's `KEY idx_x (col)` shape).
15117        let mut idx_name: Option<String> = None;
15118        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15119            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15120        {
15121            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15122                idx_name = Some(s);
15123            }
15124        }
15125        // Optional `USING BTREE` / `USING HASH` (MySQL).
15126        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15127            self.advance();
15128            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15129                self.advance();
15130            }
15131        }
15132        // Required column list `(col [, col]*)`.
15133        if !matches!(self.peek(), Token::LParen) {
15134            return Err(self.err(alloc::format!(
15135                "expected '(' in inline KEY/INDEX, got {:?}",
15136                self.peek()
15137            )));
15138        }
15139        self.advance();
15140        let mut cols: Vec<String> = Vec::new();
15141        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15142            self.advance();
15143            cols.push(s);
15144            // Skip optional `(length)` per-column prefix.
15145            if matches!(self.peek(), Token::LParen) {
15146                let mut depth = 1usize;
15147                self.advance();
15148                while depth > 0 {
15149                    match self.peek() {
15150                        Token::LParen => depth += 1,
15151                        Token::RParen => depth -= 1,
15152                        Token::Eof => break,
15153                        _ => {}
15154                    }
15155                    self.advance();
15156                }
15157            }
15158            // Skip optional ASC / DESC.
15159            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15160                || matches!(self.peek(), Token::Asc | Token::Desc)
15161            {
15162                self.advance();
15163            }
15164            if matches!(self.peek(), Token::Comma) {
15165                self.advance();
15166                continue;
15167            }
15168            break;
15169        }
15170        if matches!(self.peek(), Token::RParen) {
15171            self.advance();
15172        }
15173        // Trailing options on the inline index — comment / etc.
15174        // Skip until comma or `)`.
15175        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15176            self.advance();
15177        }
15178        if cols.is_empty() {
15179            return Ok(None);
15180        }
15181        if is_unique {
15182            // Carry the captured idx_name on UNIQUE too so future
15183            // engine work can name the underlying BTree
15184            // accordingly; today the unique-constraint installer
15185            // synthesises the name itself, but Display round-trip
15186            // benefits from preserving it.
15187            Ok(Some(crate::ast::TableConstraint::Unique {
15188                name: idx_name,
15189                columns: cols,
15190                nulls_not_distinct: false,
15191                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15192                deferrable: false,
15193                initially_deferred: false,
15194            }))
15195        } else if is_fulltext {
15196            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15197            // routes through `TableConstraint::FulltextIndex`;
15198            // the engine builds a tsvector-GIN over each named
15199            // column so MATCH AGAINST gets a real inverted
15200            // index instead of a silently-dropped declaration.
15201            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15202                name: idx_name,
15203                columns: cols,
15204            }))
15205        } else if is_spatial {
15206            // SPG has no native SPATIAL AM. Accept-as-no-op
15207            // (declaration is parsed, but no index is built).
15208            Ok(None)
15209        } else {
15210            // v7.15.0 — plain KEY / INDEX builds a real BTree
15211            // secondary index.
15212            Ok(Some(crate::ast::TableConstraint::Index {
15213                name: idx_name,
15214                columns: cols,
15215            }))
15216        }
15217    }
15218
15219    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15220    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15221    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15222    /// (in any order, separated by whitespace).
15223    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15224    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15225    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15226    /// bare ident here, and only the parenthesised form is reloptions (so this
15227    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15228    fn consume_with_reloptions(&mut self) {
15229        let is_with = matches!(
15230            self.peek(),
15231            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15232        );
15233        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15234            return;
15235        }
15236        self.advance(); // WITH
15237        self.advance(); // (
15238        let mut depth = 1u32;
15239        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15240            match self.peek() {
15241                Token::LParen => depth += 1,
15242                Token::RParen => depth -= 1,
15243                _ => {}
15244            }
15245            self.advance();
15246        }
15247    }
15248
15249    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15250    /// dropped with everything else here. The rest of the MySQL table
15251    /// options genuinely have no meaning for SPG's storage; the engine
15252    /// name does, because MySQL REFUSES one it does not know and a dump
15253    /// with a typo in it should not quietly become a table.
15254    fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15255        let mut engine: Option<alloc::string::String> = None;
15256        loop {
15257            // Heuristic: a table option is an ident (or `DEFAULT`
15258            // reserved keyword) followed by `=` and an
15259            // ident / string / integer.
15260            let name_lc = match self.peek().clone() {
15261                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15262                Token::Default => alloc::string::String::from("default"),
15263                _ => break,
15264            };
15265            let known = matches!(
15266                name_lc.as_str(),
15267                "engine"
15268                    | "default"
15269                    | "charset"
15270                    | "collate"
15271                    | "auto_increment"
15272                    | "row_format"
15273                    | "comment"
15274                    | "pack_keys"
15275                    | "stats_persistent"
15276                    | "stats_auto_recalc"
15277                    | "stats_sample_pages"
15278                    | "key_block_size"
15279                    | "tablespace"
15280                    | "min_rows"
15281                    | "max_rows"
15282                    | "checksum"
15283                    | "delay_key_write"
15284                    | "insert_method"
15285                    | "data"
15286                    | "index"
15287                    | "encryption"
15288                    | "compression"
15289            );
15290            if !known {
15291                break;
15292            }
15293            self.advance(); // option name
15294            // `DEFAULT` optional prefix is followed by `CHARSET` /
15295            // `COLLATE`; consume the next ident too.
15296            if name_lc == "default" {
15297                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15298                    self.advance();
15299                }
15300            }
15301            if matches!(self.peek(), Token::Eq) {
15302                self.advance();
15303            }
15304            match self.peek().clone() {
15305                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15306                    if name_lc == "engine" {
15307                        engine = Some(v);
15308                    }
15309                    self.advance();
15310                }
15311                Token::Integer(_) => {
15312                    self.advance();
15313                }
15314                _ => {}
15315            }
15316        }
15317        engine
15318    }
15319
15320    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15321    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15322    /// sure (otherwise a column literally named `primary` would
15323    /// be mistaken).
15324    fn peek_table_level_pk_start(&self) -> bool {
15325        let cur = self.peek();
15326        let nxt = self.tokens.get(self.pos + 1);
15327        let nxt2 = self.tokens.get(self.pos + 2);
15328        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15329        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15330        let is_lparen = matches!(nxt2, Some(Token::LParen));
15331        is_primary && is_key && is_lparen
15332    }
15333
15334    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15335    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15336    /// (mailrs round-5 G10).
15337    fn peek_table_level_unique_start(&self) -> bool {
15338        let cur = self.peek();
15339        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15340        if !is_unique {
15341            return false;
15342        }
15343        let n1 = self.tokens.get(self.pos + 1);
15344        // Plain `UNIQUE (…)`.
15345        if matches!(n1, Some(Token::LParen)) {
15346            return true;
15347        }
15348        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15349        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15350        if !is_nulls {
15351            return false;
15352        }
15353        let n2 = self.tokens.get(self.pos + 2);
15354        let n3 = self.tokens.get(self.pos + 3);
15355        let n4 = self.tokens.get(self.pos + 4);
15356        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15357        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15358            return true;
15359        }
15360        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15361        if matches!(n2, Some(Token::Not))
15362            && matches!(n3, Some(Token::Distinct))
15363            && matches!(n4, Some(Token::LParen))
15364        {
15365            return true;
15366        }
15367        false
15368    }
15369
15370    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15371        self.advance(); // PRIMARY
15372        self.advance(); // KEY
15373        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15374        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15375        // 621 consumed and dropped them (the storing half of F08).
15376        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15377        Ok(crate::ast::TableConstraint::PrimaryKey {
15378            name: None,
15379            columns,
15380            deferrable,
15381            initially_deferred,
15382        })
15383    }
15384
15385    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15386        self.advance(); // UNIQUE
15387        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15388        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15389        // is `NULLS DISTINCT` per the SQL standard.
15390        let mut nulls_not_distinct = false;
15391        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15392            let n1 = self.tokens.get(self.pos + 1);
15393            let n2 = self.tokens.get(self.pos + 2);
15394            let is_not = matches!(n1, Some(Token::Not));
15395            let is_distinct = matches!(n2, Some(Token::Distinct));
15396            if is_not && is_distinct {
15397                self.advance(); // NULLS
15398                self.advance(); // NOT
15399                self.advance(); // DISTINCT
15400                nulls_not_distinct = true;
15401            } else if matches!(n1, Some(Token::Distinct)) {
15402                self.advance(); // NULLS
15403                self.advance(); // DISTINCT
15404            }
15405        }
15406        let columns = self.parse_paren_ident_list("UNIQUE")?;
15407        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15408        Ok(crate::ast::TableConstraint::Unique {
15409            name: None,
15410            columns,
15411            nulls_not_distinct,
15412            deferrable,
15413            initially_deferred,
15414        })
15415    }
15416
15417    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15418    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15419    /// expression.
15420    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15421    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15422    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15423    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15424    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15425    /// commit: `NOT` starts no other suffix here, but reading both
15426    /// tokens before advancing keeps the caller's error message intact
15427    /// if someone writes `NOT NULL` by mistake.
15428    fn parse_not_valid_suffix(&mut self) -> bool {
15429        if !matches!(self.peek(), Token::Not) {
15430            return false;
15431        }
15432        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15433        {
15434            return false;
15435        }
15436        self.advance();
15437        self.advance();
15438        true
15439    }
15440
15441    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15442        self.advance(); // EXCLUDE
15443        // Optional `USING <method>`.
15444        let mut method = None;
15445        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15446            self.advance();
15447            method = Some(match self.advance() {
15448                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15449                other => {
15450                    return Err(self.err(alloc::format!(
15451                        "expected index method after USING, got {other:?}"
15452                    )));
15453                }
15454            });
15455        }
15456        if !matches!(self.peek(), Token::LParen) {
15457            return Err(self.err(alloc::format!(
15458                "expected '(' after EXCLUDE, got {:?}",
15459                self.peek()
15460            )));
15461        }
15462        self.advance();
15463        let mut elements: Vec<(String, String)> = Vec::new();
15464        loop {
15465            let col = match self.advance() {
15466                Token::Ident(s) | Token::QuotedIdent(s) => s,
15467                other => {
15468                    return Err(self.err(alloc::format!(
15469                        "expected column name in EXCLUDE, got {other:?}"
15470                    )));
15471                }
15472            };
15473            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15474                return Err(self.err(alloc::format!(
15475                    "expected WITH after EXCLUDE column, got {:?}",
15476                    self.peek()
15477                )));
15478            }
15479            self.advance();
15480            let op = match self.advance() {
15481                Token::InetOverlap => String::from("&&"),
15482                Token::Intersects => String::from("?#"),
15483                Token::IsBelow => String::from("<^"),
15484                Token::IsAbove => String::from(">^"),
15485                Token::PatternLt => String::from("~<~"),
15486                Token::PatternLtEq => String::from("~<=~"),
15487                Token::PatternGt => String::from("~>~"),
15488                Token::PatternGtEq => String::from("~>=~"),
15489                Token::TsMatchOld => String::from("@@@"),
15490                Token::Eq => String::from("="),
15491                Token::JsonContains => String::from("@>"),
15492                Token::JsonContainedBy => String::from("<@"),
15493                Token::OverLeft => String::from("&<"),
15494                Token::OverRight => String::from("&>"),
15495                other => {
15496                    return Err(self.err(alloc::format!(
15497                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15498                    )));
15499                }
15500            };
15501            elements.push((col, op));
15502            if matches!(self.peek(), Token::Comma) {
15503                self.advance();
15504                continue;
15505            }
15506            break;
15507        }
15508        if !matches!(self.peek(), Token::RParen) {
15509            return Err(self.err(alloc::format!(
15510                "expected ')' to close EXCLUDE, got {:?}",
15511                self.peek()
15512            )));
15513        }
15514        self.advance();
15515        Ok(crate::ast::TableConstraint::Exclude {
15516            name: None,
15517            method,
15518            elements,
15519        })
15520    }
15521
15522    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15523        self.advance(); // CHECK
15524        if !matches!(self.peek(), Token::LParen) {
15525            return Err(self.err(alloc::format!(
15526                "expected '(' after CHECK, got {:?}",
15527                self.peek()
15528            )));
15529        }
15530        self.advance();
15531        let expr = self.parse_expr(0)?;
15532        if !matches!(self.peek(), Token::RParen) {
15533            return Err(self.err(alloc::format!(
15534                "expected ')' to close CHECK predicate, got {:?}",
15535                self.peek()
15536            )));
15537        }
15538        self.advance();
15539        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15540        // are no existing rows for PG to skip, so it rejects the suffix.
15541        Ok(crate::ast::TableConstraint::Check {
15542            name: None,
15543            expr,
15544            not_valid: false,
15545        })
15546    }
15547
15548    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15549    fn peek_table_level_check_start(&self) -> bool {
15550        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15551    }
15552
15553    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15554    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15555    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15556    /// own CONSTRAINT prefix).
15557    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15558        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15559            return None;
15560        }
15561        // tokens[pos+1] is the constraint name (any ident-like);
15562        // tokens[pos+2] is the kind keyword.
15563        match self.tokens.get(self.pos + 2) {
15564            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15565                Some(NamedTableConstraintKind::Check)
15566            }
15567            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15568                Some(NamedTableConstraintKind::Unique)
15569            }
15570            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15571                Some(NamedTableConstraintKind::PrimaryKey)
15572            }
15573            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15574                Some(NamedTableConstraintKind::Exclude)
15575            }
15576            _ => None,
15577        }
15578    }
15579
15580    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15581        if !matches!(self.peek(), Token::LParen) {
15582            return Err(self.err(alloc::format!(
15583                "expected '(' after {ctx}, got {:?}",
15584                self.peek()
15585            )));
15586        }
15587        self.advance();
15588        let mut out = Vec::new();
15589        loop {
15590            out.push(self.expect_ident_like()?);
15591            match self.peek() {
15592                Token::Comma => {
15593                    self.advance();
15594                }
15595                Token::RParen => {
15596                    self.advance();
15597                    break;
15598                }
15599                other => {
15600                    return Err(self.err(alloc::format!(
15601                        "expected ',' or ')' in {ctx} list, got {other:?}"
15602                    )));
15603                }
15604            }
15605        }
15606        if out.is_empty() {
15607            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15608        }
15609        Ok(out)
15610    }
15611
15612    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15613    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15614    /// table-level FK; a column def never starts with either keyword
15615    /// (column names are not in this reserved set).
15616    fn peek_constraint_or_fk_start(&self) -> bool {
15617        let is_constraint_kw = matches!(
15618            self.peek(),
15619            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15620        );
15621        let is_foreign_kw = matches!(
15622            self.peek(),
15623            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15624        );
15625        is_constraint_kw || is_foreign_kw
15626    }
15627
15628    /// v7.6.0 — parse a table-level FK clause:
15629    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15630    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15631    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15632        let mut name: Option<String> = None;
15633        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15634            self.advance();
15635            name = Some(self.expect_ident_like()?);
15636        }
15637        // `FOREIGN`
15638        match self.advance() {
15639            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15640            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15641        }
15642        // `KEY`
15643        match self.advance() {
15644            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15645            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15646        }
15647        // `(col, col, ...)`
15648        if !matches!(self.peek(), Token::LParen) {
15649            return Err(self.err(format!(
15650                "expected '(' after FOREIGN KEY, got {:?}",
15651                self.peek()
15652            )));
15653        }
15654        self.advance();
15655        let mut columns = Vec::new();
15656        loop {
15657            columns.push(self.expect_ident_like()?);
15658            match self.peek() {
15659                Token::Comma => {
15660                    self.advance();
15661                }
15662                Token::RParen => {
15663                    self.advance();
15664                    break;
15665                }
15666                other => {
15667                    return Err(self.err(format!(
15668                        "expected ',' or ')' in FK column list, got {other:?}"
15669                    )));
15670                }
15671            }
15672        }
15673        if columns.is_empty() {
15674            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15675        }
15676        let (
15677            parent_table,
15678            parent_columns,
15679            on_delete,
15680            on_update,
15681            match_type,
15682            deferrable,
15683            initially_deferred,
15684        ) = self.parse_references_tail(columns.len())?;
15685        Ok(ForeignKeyConstraint {
15686            name,
15687            columns,
15688            parent_table,
15689            parent_columns,
15690            on_delete,
15691            on_update,
15692            match_type,
15693            deferrable,
15694            initially_deferred,
15695        })
15696    }
15697
15698    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15699    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15700    /// the local column count, used to default the parent column
15701    /// list when omitted (SQL spec: parent's PK is implied).
15702    fn parse_references_tail(
15703        &mut self,
15704        expected_arity: usize,
15705    ) -> Result<
15706        (
15707            String,
15708            Vec<String>,
15709            FkAction,
15710            FkAction,
15711            crate::ast::MatchType,
15712            // v7.39 (round 288) — deferrable, initially_deferred.
15713            bool,
15714            bool,
15715        ),
15716        ParseError,
15717    > {
15718        match self.advance() {
15719            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15720            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15721        }
15722        let parent_table = self.expect_ident_like()?;
15723        let mut parent_columns: Vec<String> = Vec::new();
15724        if matches!(self.peek(), Token::LParen) {
15725            self.advance();
15726            loop {
15727                parent_columns.push(self.expect_ident_like()?);
15728                match self.peek() {
15729                    Token::Comma => {
15730                        self.advance();
15731                    }
15732                    Token::RParen => {
15733                        self.advance();
15734                        break;
15735                    }
15736                    other => {
15737                        return Err(self.err(format!(
15738                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15739                        )));
15740                    }
15741                }
15742            }
15743        }
15744        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15745            return Err(self.err(format!(
15746                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15747                expected_arity,
15748                parent_columns.len()
15749            )));
15750        }
15751        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15752        // it between the referenced column list and the ON / DEFERRABLE
15753        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15754        // is skipped when any referencing column is NULL), so SIMPLE —
15755        // the default, and the only spelling pg_dump emits — is accepted
15756        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15757        // mixed-NULL rule, which is not wired yet; reject them honestly
15758        // rather than silently applying SIMPLE (PG itself errors on
15759        // MATCH PARTIAL as "not yet implemented").
15760        let mut match_type = crate::ast::MatchType::Simple;
15761        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15762            self.advance();
15763            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15764            // SIMPLE / PARTIAL arrive as bare identifiers.
15765            let kind = match self.advance() {
15766                Token::Full => "FULL".to_string(),
15767                Token::Ident(s) => s.to_uppercase(),
15768                other => {
15769                    return Err(self.err(format!(
15770                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15771                    )));
15772                }
15773            };
15774            match kind.as_str() {
15775                "SIMPLE" => {} // Default — match_type stays Simple.
15776                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15777                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15778                "FULL" => match_type = crate::ast::MatchType::Full,
15779                "PARTIAL" => {
15780                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15781                }
15782                _ => {
15783                    return Err(self.err(format!(
15784                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15785                    )));
15786                }
15787            }
15788        }
15789        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15790        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15791        // <action>` / `ON UPDATE <action>` in either order. PG /
15792        // pg_dump emits the timing clause AFTER the ON clauses
15793        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15794        // but the SQL spec allows either order. We loop over
15795        // every possible trailer and dispatch on the next token,
15796        // stopping when nothing matches. Phase 3.1 changes the
15797        // bare DEFERRABLE form from hard-error to accept-as-
15798        // immediate; SPG is single-writer with no deferred-
15799        // constraint window so the runtime semantics are always
15800        // immediate even when INITIALLY DEFERRED is requested.
15801        // PG's default referential action (no ON DELETE / ON UPDATE
15802        // clause) is NO ACTION, not RESTRICT — the two enforce
15803        // identically in SPG (single-writer, no deferred window; see the
15804        // shared match arm in constraints.rs) but information_schema.
15805        // referential_constraints must report NO ACTION to match PG.
15806        let mut on_delete = FkAction::NoAction;
15807        let mut on_update = FkAction::NoAction;
15808        let mut seen_on_delete = false;
15809        let mut seen_on_update = false;
15810        let mut deferrable = false;
15811        let mut initially_deferred = false;
15812        loop {
15813            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15814            let before = self.pos;
15815            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15816            if self.pos != before {
15817                deferrable = d;
15818                initially_deferred = idef;
15819                continue;
15820            }
15821            // ON DELETE / ON UPDATE.
15822            if !matches!(self.peek(), Token::On) {
15823                break;
15824            }
15825            self.advance();
15826            let which = self.advance();
15827            let action = self.parse_fk_action()?;
15828            match which {
15829                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15830                    if seen_on_delete {
15831                        return Err(self.err("ON DELETE specified twice".into()));
15832                    }
15833                    seen_on_delete = true;
15834                    on_delete = action;
15835                }
15836                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15837                    if seen_on_update {
15838                        return Err(self.err("ON UPDATE specified twice".into()));
15839                    }
15840                    seen_on_update = true;
15841                    on_update = action;
15842                }
15843                other => {
15844                    return Err(
15845                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15846                    );
15847                }
15848            }
15849        }
15850        Ok((
15851            parent_table,
15852            parent_columns,
15853            on_delete,
15854            on_update,
15855            match_type,
15856            deferrable,
15857            initially_deferred,
15858        ))
15859    }
15860
15861    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15862    /// NO ACTION`.
15863    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15864        match self.advance() {
15865            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15866            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15867            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15868                Token::Null => Ok(FkAction::SetNull),
15869                Token::Default => Ok(FkAction::SetDefault),
15870                other => Err(self.err(format!(
15871                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15872                ))),
15873            },
15874            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15875                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15876                other => Err(self.err(format!(
15877                    "expected ACTION after NO in FK action, got {other:?}"
15878                ))),
15879            },
15880            other => Err(self.err(format!(
15881                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15882            ))),
15883        }
15884    }
15885
15886    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15887    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15888    fn consume_if_not_exists(&mut self) -> bool {
15889        // `IF` arrives as a bare Ident (we don't reserve it because it
15890        // also appears mid-expression in PG, though we don't support
15891        // those forms yet).
15892        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15893        if !looks_like_if {
15894            return false;
15895        }
15896        // Peek one ahead before committing: only consume IF when it's
15897        // actually `IF NOT EXISTS`.
15898        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15899            return false;
15900        }
15901        if !matches!(
15902            self.tokens.get(self.pos + 2),
15903            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15904        ) {
15905            return false;
15906        }
15907        self.advance(); // IF
15908        self.advance(); // NOT
15909        self.advance(); // EXISTS
15910        true
15911    }
15912
15913    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15914    /// Consumes IF EXISTS as a pair; returns false otherwise
15915    /// without consuming any tokens.
15916    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15917    /// ENABLE/DISABLE/FORCE/NO FORCE.
15918    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15919        for kw in ["row", "level", "security"] {
15920            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15921            {
15922                return Err(self.err(alloc::format!(
15923                    "expected {} in ROW LEVEL SECURITY, got {:?}",
15924                    kw.to_ascii_uppercase(),
15925                    self.peek()
15926                )));
15927            }
15928            self.advance();
15929        }
15930        Ok(())
15931    }
15932
15933    fn consume_if_exists(&mut self) -> bool {
15934        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15935        if !looks_like_if {
15936            return false;
15937        }
15938        if !matches!(
15939            self.tokens.get(self.pos + 1),
15940            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15941        ) {
15942            return false;
15943        }
15944        self.advance(); // IF
15945        self.advance(); // EXISTS
15946        true
15947    }
15948
15949    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15950    /// qualifiers after an index column ref. ASC / DESC are
15951    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15952    /// We accept and discard them since single-column BTree
15953    /// stores rows in natural key order today.
15954    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15955    /// ORDER BY key. Returns None when absent.
15956    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15957        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15958            return Ok(None);
15959        }
15960        self.advance();
15961        match self.advance() {
15962            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15963            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15964            other => Err(self.err(alloc::format!(
15965                "expected FIRST or LAST after NULLS, got {other:?}"
15966            ))),
15967        }
15968    }
15969
15970    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15971    /// rather than discarded.
15972    ///
15973    /// SPG's index does not scan in a direction — column ordering is
15974    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15975    /// reproduction of the DDL, and dropping the clause meant
15976    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15977    /// dump lost it, and a schema diff saw drift on every run.
15978    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15979        let mut order = crate::ast::IndexColumnOrder::default();
15980        loop {
15981            match self.peek() {
15982                Token::Asc => {
15983                    self.advance();
15984                }
15985                Token::Desc => {
15986                    order.descending = true;
15987                    self.advance();
15988                }
15989                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15990                    let look = self.tokens.get(self.pos + 1);
15991                    if matches!(
15992                        look,
15993                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15994                            || k.eq_ignore_ascii_case("last")
15995                    ) {
15996                        self.advance();
15997                        order.nulls_first = Some(matches!(
15998                            self.advance(),
15999                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16000                        ));
16001                    } else {
16002                        break;
16003                    }
16004                }
16005                _ => break,
16006            }
16007        }
16008        order
16009    }
16010
16011    fn parse_create_index_stmt_after_create(
16012        &mut self,
16013        is_unique: bool,
16014    ) -> Result<Statement, ParseError> {
16015        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16016        debug_assert!(matches!(self.peek(), Token::Index));
16017        self.advance();
16018        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16019        // SPG's CREATE INDEX is synchronous end-to-end today (real
16020        // CONCURRENTLY variant with restartable scans queues with
16021        // v7.39 indexes epic), so the modifier has no runtime effect
16022        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16023        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16024        // VIEW CONCURRENTLY.
16025        let mut concurrently = false;
16026        if matches!(
16027            self.peek(),
16028            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16029        ) {
16030            self.advance();
16031            concurrently = true;
16032        }
16033        let if_not_exists = self.consume_if_not_exists();
16034        // v7.39 (read01 round 93) — the index name is optional (PG since
16035        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16036        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16037        // was given; leave it empty and the engine derives a PG-style
16038        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16039        let name = if matches!(self.peek(), Token::On) {
16040            String::new()
16041        } else {
16042            self.expect_ident_like()?
16043        };
16044        if !matches!(self.peek(), Token::On) {
16045            return Err(self.err(format!(
16046                "expected ON after CREATE INDEX <name>, got {:?}",
16047                self.peek()
16048            )));
16049        }
16050        self.advance();
16051        let table = self.expect_ident_like()?;
16052        // Optional `USING <method>` — only recognised method in v2.0 is
16053        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16054        // ident `using` (we don't promote it to a reserved keyword
16055        // because it isn't reserved anywhere else in our SQL surface).
16056        let mut method_name: Option<String> = None;
16057        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16058            self.advance();
16059            let m = self.expect_ident_like()?;
16060            method_name = Some(m.to_ascii_lowercase());
16061            match m.to_ascii_lowercase().as_str() {
16062                "hnsw" => IndexMethod::Hnsw,
16063                "btree" => IndexMethod::BTree,
16064                "brin" => IndexMethod::Brin,
16065                // v7.12.3 — real GIN inverted index over `tsvector`.
16066                // v7.9.26b's `USING gin` → BTree silent fallback is
16067                // gone; the engine validates that the indexed column
16068                // is `tsvector` at CREATE INDEX time.
16069                "gin" => IndexMethod::Gin,
16070                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16071                // `USING spgist` / `USING hash` for their built-in
16072                // AMs that SPG doesn't have a matching
16073                // implementation for; degrade to BTree on the
16074                // leading column so the schema loads + the index
16075                // catalogue stays consistent. Operator pays the
16076                // planner cost only for the queries that would have
16077                // used the specialised AM.
16078                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16079                // v7.11.3 — pgvector ships both `ivfflat` and
16080                // `hnsw`. Customers shouldn't have to choose
16081                // their on-disk index method based on what SPG
16082                // implements; accept `ivfflat` as a synonym for
16083                // `hnsw` so PG schemas using either method drop
16084                // in. The vector distance op (`<->` / `<#>` /
16085                // `<=>`) at query time still picks the metric.
16086                "ivfflat" => IndexMethod::Hnsw,
16087                other => {
16088                    return Err(self.err(alloc::format!(
16089                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16090                    )));
16091                }
16092            }
16093        } else {
16094            IndexMethod::BTree
16095        };
16096        if !matches!(self.peek(), Token::LParen) {
16097            return Err(self.err(format!(
16098                "expected '(' before indexed column, got {:?}",
16099                self.peek()
16100            )));
16101        }
16102        self.advance();
16103        // v6.8.2 — accept either a bare column ident (legacy) or
16104        // an expression `fn(col, …)` for expression indexes.
16105        // Distinguish by peeking the token *after* the current
16106        // ident: `ident )` is the legacy column-only path;
16107        // anything else triggers the Pratt expression parser.
16108        // (`advance()` uses `mem::replace` to nil out the current
16109        // slot, so we can't save+rewind cleanly — peek-ahead via
16110        // direct index avoids the mutation.)
16111        let mut opclass: Option<String> = None;
16112        let mut key_collation: Option<String> = None;
16113        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16114            // Single column with `)` immediately after — fast path.
16115            // v7.9.29 — also: bare column followed by `,` (the
16116            // multi-column form `(a, b, c)`). Without this branch
16117            // the leading ident gets pulled into `parse_expr`
16118            // which then sets `expression = Some(Column(a))` and
16119            // breaks Display round-trip on the multi-column shape.
16120            Token::Ident(s) | Token::QuotedIdent(s)
16121                if matches!(
16122                    self.tokens.get(self.pos + 1),
16123                    Some(Token::RParen | Token::Comma)
16124                ) =>
16125            {
16126                self.advance();
16127                (s, None)
16128            }
16129            // v7.9.22 — single column followed by a pgvector
16130            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16131            // v7.15.0 — capture the opclass instead of discarding
16132            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16133            // → real trigram-shingle GIN over a TEXT column).
16134            // Vector/HNSW opclasses still take their distance
16135            // metric from the query operator (`<->` / `<#>` /
16136            // `<=>`), so for those callers the opclass stays
16137            // informational.
16138            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16139            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16140            // the schema and dispatch on the bare opclass, the same
16141            // treatment table/type names get.
16142            Token::Ident(s) | Token::QuotedIdent(s)
16143                if matches!(
16144                    self.tokens.get(self.pos + 1),
16145                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16146                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16147                    && matches!(
16148                        self.tokens.get(self.pos + 3),
16149                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16150                            if is_vector_opclass_name(op)
16151                    ) =>
16152            {
16153                self.advance(); // column name
16154                self.advance(); // schema qualifier
16155                self.advance(); // dot
16156                let op_tok = self.advance();
16157                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16158                    opclass = Some(op.to_ascii_lowercase());
16159                }
16160                (s, None)
16161            }
16162            // r1038 — an operator class is recognised by its POSITION, not
16163            // by a list of names. It used to be `is_vector_opclass_name`,
16164            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16165            // sentori's migration wrote — was a syntax error while
16166            // `USING gin (doc)` parsed. Anything sitting between a column
16167            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16168            // two bare identifiers in a row are not valid there otherwise.
16169            Token::Ident(s) | Token::QuotedIdent(s)
16170                if matches!(
16171                    self.tokens.get(self.pos + 1),
16172                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16173                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16174                            self.tokens.get(self.pos + 2)
16175                        )
16176                ) =>
16177            {
16178                self.advance(); // column name
16179                // Capture the opclass token, lower-cased for
16180                // case-insensitive engine dispatch.
16181                let op_tok = self.advance();
16182                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16183                    opclass = Some(op.to_ascii_lowercase());
16184                }
16185                (s, None)
16186            }
16187            Token::Ident(_) | Token::QuotedIdent(_) => {
16188                // v7.39 (round 538) — an explicit COLLATE on the key,
16189                // read by LOOKAHEAD because `parse_expr` absorbs the
16190                // clause as a no-op (SPG orders text by bytes, which is
16191                // the C collation, so it changes nothing to honour). PG
16192                // still PRINTS it: an explicitly written `"C"` and the
16193                // collation a column inherits are different collation
16194                // OBJECTS even where they sort identically, which is why
16195                // `(a COLLATE "C")` shows on a C-collation database too.
16196                if matches!(
16197                    self.tokens.get(self.pos + 1),
16198                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16199                ) {
16200                    key_collation = match self.tokens.get(self.pos + 2) {
16201                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16202                            Some(n.clone())
16203                        }
16204                        _ => None,
16205                    };
16206                }
16207                let key_expr = self.parse_expr(0)?;
16208                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16209                    self.err("expression index key must reference at least one column".into())
16210                })?;
16211                (primary, Some(key_expr))
16212            }
16213            // v7.37.43-T4 — parenthesised expression index key
16214            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16215            // PG's CREATE INDEX requires the expression to be in
16216            // its own parens to disambiguate function calls from
16217            // column lists, so this `LParen` is the inner open-paren
16218            // of an expression key. parse_expr handles the recursive
16219            // descent and consumes the matching `RParen`.
16220            Token::LParen => {
16221                let key_expr = self.parse_expr(0)?;
16222                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16223                    self.err("expression index key must reference at least one column".into())
16224                })?;
16225                (primary, Some(key_expr))
16226            }
16227            other => {
16228                return Err(self.err(format!(
16229                    "expected column ident or expression, got {other:?}"
16230                )));
16231            }
16232        };
16233        // v7.9.14 — accept extra comma-separated columns inside
16234        // the index key parens (`CREATE INDEX … (a, b, c)`).
16235        // mailrs F2. Each extra column may carry an optional
16236        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16237        // — parsed and discarded; SPG doesn't honour direction
16238        // on a BTree index today (column ordering is intrinsic
16239        // to the storage). v7.10 will widen to genuine composite
16240        // index keys.
16241        let mut extra_columns: Vec<String> = Vec::new();
16242        // The leading column may also have ASC/DESC after it — and that
16243        // one is the column SPG indexes, so its clause is kept.
16244        let key_order = self.consume_optional_index_column_qualifiers();
16245        while matches!(self.peek(), Token::Comma) {
16246            self.advance();
16247            let extra = self.expect_ident_like()?;
16248            let _ = self.consume_optional_index_column_qualifiers();
16249            extra_columns.push(extra);
16250        }
16251        if !matches!(self.peek(), Token::RParen) {
16252            return Err(self.err(format!(
16253                "expected ')' after indexed column / expression, got {:?}",
16254                self.peek()
16255            )));
16256        }
16257        self.advance();
16258        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16259        // index-only-scan annotation. Bare ident (not a reserved
16260        // keyword) so we test by case-insensitive string match.
16261        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16262        {
16263            self.advance();
16264            if !matches!(self.peek(), Token::LParen) {
16265                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16266            }
16267            self.advance();
16268            let mut cols = Vec::new();
16269            loop {
16270                cols.push(self.expect_ident_like()?);
16271                match self.peek() {
16272                    Token::Comma => {
16273                        self.advance();
16274                    }
16275                    Token::RParen => {
16276                        self.advance();
16277                        break;
16278                    }
16279                    other => {
16280                        return Err(self.err(format!(
16281                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16282                        )));
16283                    }
16284                }
16285            }
16286            cols
16287        } else {
16288            Vec::new()
16289        };
16290        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16291        // storage parameters. pgvector emits `WITH (lists = N)` for
16292        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16293        // SPG's HNSW picks its own parameters today (tunable via
16294        // env vars), so the WITH clause is informational and dropped.
16295        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16296            self.advance();
16297            if !matches!(self.peek(), Token::LParen) {
16298                return Err(self.err(format!(
16299                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16300                    self.peek()
16301                )));
16302            }
16303            self.advance();
16304            loop {
16305                if matches!(self.peek(), Token::RParen) {
16306                    self.advance();
16307                    break;
16308                }
16309                // Drain `key = value` or bare `key` tokens.
16310                let _ = self.advance(); // key
16311                if matches!(self.peek(), Token::Eq) {
16312                    self.advance();
16313                    let _ = self.advance(); // value (int / string / ident)
16314                }
16315                match self.peek() {
16316                    Token::Comma => {
16317                        self.advance();
16318                    }
16319                    Token::RParen => {
16320                        self.advance();
16321                        break;
16322                    }
16323                    other => {
16324                        return Err(self.err(format!(
16325                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16326                        )));
16327                    }
16328                }
16329            }
16330        }
16331        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16332        // which sits between the key list and the WHERE clause.
16333        let mut nulls_not_distinct = false;
16334        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16335            let n1 = self.tokens.get(self.pos + 1);
16336            let n2 = self.tokens.get(self.pos + 2);
16337            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16338                self.advance(); // NULLS
16339                self.advance(); // NOT
16340                self.advance(); // DISTINCT
16341                nulls_not_distinct = true;
16342            } else if matches!(n1, Some(Token::Distinct)) {
16343                self.advance(); // NULLS
16344                self.advance(); // DISTINCT
16345            }
16346        }
16347        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16348        let partial_predicate = if matches!(self.peek(), Token::Where) {
16349            self.advance();
16350            Some(self.parse_expr(0)?)
16351        } else {
16352            None
16353        };
16354        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16355        // sense: uniqueness over an ANN structure has no clean
16356        // semantics. Reject early. (BRIN UNIQUE is similarly
16357        // meaningless — block both.)
16358        if is_unique && !matches!(method, IndexMethod::BTree) {
16359            return Err(self.err(alloc::format!(
16360                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16361                method
16362            )));
16363        }
16364        Ok(Statement::CreateIndex(CreateIndexStatement {
16365            concurrently,
16366            name,
16367            key_order,
16368            key_collation,
16369            table,
16370            column,
16371            nulls_not_distinct,
16372            method,
16373            if_not_exists,
16374            included_columns,
16375            partial_predicate,
16376            extra_columns: extra_columns.clone(),
16377            expression,
16378            is_unique,
16379            opclass,
16380            method_name,
16381        }))
16382    }
16383
16384    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16385    /// column-level `REFERENCES ...` clause. The trailing FK is
16386    /// normalised into table-level shape (single-element columns +
16387    /// parent_columns) so the engine sees one uniform constraint list.
16388    fn parse_column_def_with_fk(
16389        &mut self,
16390    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16391        let col = self.parse_column_def()?;
16392        // v7.39 (round 308, V29) — an explicitly named inline FK:
16393        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16394        // loop leaves this spelling intact precisely so the name can be
16395        // kept here; PG reports it in violation messages and matches it
16396        // in `SET CONSTRAINTS`.
16397        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16398        {
16399            self.advance();
16400            Some(self.expect_ident_like()?)
16401        } else {
16402            None
16403        };
16404        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16405        let inline_references = matches!(
16406            self.peek(),
16407            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16408        );
16409        if !inline_references {
16410            return Ok((col, None));
16411        }
16412        let (
16413            parent_table,
16414            parent_columns,
16415            on_delete,
16416            on_update,
16417            match_type,
16418            deferrable,
16419            initially_deferred,
16420        ) = self.parse_references_tail(1)?;
16421        let fk = ForeignKeyConstraint {
16422            name: declared_name,
16423            columns: vec![col.name.clone()],
16424            parent_table,
16425            parent_columns,
16426            on_delete,
16427            on_update,
16428            match_type,
16429            deferrable,
16430            initially_deferred,
16431        };
16432        Ok((col, Some(fk)))
16433    }
16434
16435    /// v7.13.0 — parse a column type (consuming the type ident and
16436    /// any trailing parameters / `[]`), without surrounding column
16437    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16438    /// Returns the resolved `ColumnTypeName` plus implied
16439    /// `(auto_increment, not_null)` flags from PG SERIAL family
16440    /// shorthands — callers that don't expect those (ALTER COLUMN
16441    /// TYPE) can discard them.
16442    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16443        let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16444        Ok(ty)
16445    }
16446
16447    #[allow(clippy::type_complexity)]
16448    fn parse_type_with_implied_flags(
16449        &mut self,
16450    ) -> Result<
16451        (
16452            ColumnTypeName,
16453            bool,
16454            bool,
16455            Option<String>,
16456            Collation,
16457            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16458            bool,
16459            // v7.39 (round 676) — the collation NAME as written, which the
16460            // `Collation` enum above cannot carry.
16461            Option<String>,
16462            bool,
16463            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16464            // list captured at type-parse time. None for all
16465            // non-ENUM types.
16466            Option<Vec<String>>,
16467            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16468            // list. Distinct from ENUM (subset semantics).
16469            Option<Vec<String>>,
16470            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16471            // width, lost when the type collapses to SmallInt / Int.
16472            Option<MysqlIntWidth>,
16473            // v7.39 (round 424) — declared fractional-seconds precision of a
16474            // MySQL temporal column (bare spelling = 0). None under PG.
16475            Option<u8>,
16476        ),
16477        ParseError,
16478    > {
16479        let mut ty_ident = match self.advance() {
16480            Token::Ident(s) => s,
16481            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16482            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16483            // '<span>'` literal grammar. As a column type it lands
16484            // here directly; downstream resolution still uses the
16485            // canonical lowercase string.
16486            Token::Interval => "interval".to_string(),
16487            other => {
16488                return Err(ParseError {
16489                    message: format!("expected column type, got {other:?}"),
16490                    token_pos: self.consumed_pos(),
16491                });
16492            }
16493        };
16494        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16495        // pg_dump qualifies extension types (`public.vector(1024)`).
16496        // SPG is single-namespace; drop the schema and resolve the
16497        // bare type — same treatment table names already get.
16498        while matches!(self.peek(), Token::Dot) {
16499            self.advance();
16500            ty_ident = self.expect_ident_like()?;
16501        }
16502        let mut implied_auto_increment = false;
16503        let mut implied_not_null = false;
16504        let mut user_type_ref: Option<String> = None;
16505        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16506        // value list, captured here and bubbled up through the
16507        // ColumnDef so the engine can attach it to the column
16508        // schema (and validate INSERT cells against it).
16509        let mut inline_enum_variants: Option<Vec<String>> = None;
16510        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16511        let mut inline_set_variants: Option<Vec<String>> = None;
16512        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16513        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16514        // collapses to SmallInt / Int. Only under the MySQL dialect.
16515        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16516        // v7.39 (round 424) — the declared fractional-seconds precision of a
16517        // MySQL temporal column. Set by the temporal arms below; stays None
16518        // for PG (whose temporal columns keep full microseconds).
16519        let mut mysql_fsp: Option<u8> = None;
16520        let mut ty = match ty_ident.as_str() {
16521            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16522            "smallserial" | "serial2" => {
16523                implied_auto_increment = true;
16524                implied_not_null = true;
16525                ColumnTypeName::SmallInt
16526            }
16527            "serial" | "serial4" => {
16528                implied_auto_increment = true;
16529                implied_not_null = true;
16530                ColumnTypeName::Int
16531            }
16532            "bigserial" | "serial8" => {
16533                implied_auto_increment = true;
16534                implied_not_null = true;
16535                ColumnTypeName::BigInt
16536            }
16537            // MySQL flavours we accept by aliasing to the closest SPG
16538            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16539            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16540            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16541            // without semantic effect.
16542            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16543            // PG's internal type names; pg_dump and hand-written PG schemas
16544            // use them interchangeably with smallint / int / bigint (the cast
16545            // path already accepted them, only the column grammar didn't).
16546            "smallint" | "int2" => {
16547                // v7.14.0 — MySQL display-width on integers
16548                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16549                // parenthesised number is purely cosmetic — it
16550                // doesn't change storage. Accept + discard.
16551                self.consume_optional_paren_size();
16552                ColumnTypeName::SmallInt
16553            }
16554            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16555            // canonical encoding for BOOLEAN. Every MySQL driver
16556            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16557            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16558            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16559            // gave the customer i16-shaped values where the app
16560            // expected bool — a Tier-A silent type drift on
16561            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16562            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16563            // stay SmallInt (the legacy width-agnostic path).
16564            "tinyint" => {
16565                let width = self.peek_optional_paren_size_value();
16566                self.consume_optional_paren_size();
16567                if width == Some(1) {
16568                    ColumnTypeName::Bool
16569                } else {
16570                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16571                    // lost width so the write path can enforce -128..127.
16572                    if self.mysql_dialect {
16573                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16574                    }
16575                    ColumnTypeName::SmallInt
16576                }
16577            }
16578            "mediumint" => {
16579                self.consume_optional_paren_size();
16580                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16581                if self.mysql_dialect {
16582                    mysql_int_width = Some(MysqlIntWidth::Medium);
16583                }
16584                ColumnTypeName::Int
16585            }
16586            "int" | "integer" | "int4" => {
16587                self.consume_optional_paren_size();
16588                ColumnTypeName::Int
16589            }
16590            "bigint" | "int8" => {
16591                self.consume_optional_paren_size();
16592                ColumnTypeName::BigInt
16593            }
16594            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16595            // (mailrs round-5 G6). Consume the optional `PRECISION`
16596            // tail when the type keyword was `double` / `DOUBLE`.
16597            //
16598            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16599            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16600            // p in 1..=24 is real, 25..=53 is double precision, and
16601            // anything else is an error.
16602            "float" | "double" | "real" => {
16603                if ty_ident.eq_ignore_ascii_case("double")
16604                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16605                {
16606                    self.advance();
16607                }
16608                if ty_ident.eq_ignore_ascii_case("real") {
16609                    // v7.39 (round 274) — the two dialects genuinely
16610                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16611                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16612                    // 32-bit globally and thereby narrowed the stored
16613                    // precision of every MySQL REAL column.
16614                    if self.mysql_dialect {
16615                        ColumnTypeName::Float
16616                    } else {
16617                        ColumnTypeName::Real
16618                    }
16619                } else if ty_ident.eq_ignore_ascii_case("float")
16620                    && self.mysql_dialect
16621                    && matches!(self.peek(), Token::LParen)
16622                    && self.peek_paren_has_comma()
16623                {
16624                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16625                    // display form (`FLOAT(10,2)`), which PG has no
16626                    // equivalent of. It was `syntax error at or near ","`,
16627                    // so the whole CREATE failed. The digits are a display
16628                    // hint only; SPG stores the full double.
16629                    self.consume_optional_paren_size();
16630                    ColumnTypeName::Float
16631                } else if ty_ident.eq_ignore_ascii_case("float")
16632                    && matches!(self.peek(), Token::LParen)
16633                {
16634                    // PG words the two bounds differently, and
16635                    // parse_paren_size already rejects a zero.
16636                    let p = self.parse_paren_size("FLOAT")?;
16637                    if p > 53 {
16638                        return Err(self.err(String::from(
16639                            "precision for type float must be less than 54 bits",
16640                        )));
16641                    }
16642                    if p <= 24 {
16643                        ColumnTypeName::Real
16644                    } else {
16645                        ColumnTypeName::Float
16646                    }
16647                } else {
16648                    ColumnTypeName::Float
16649                }
16650            }
16651            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16652            "float4" => ColumnTypeName::Real,
16653            "float8" => ColumnTypeName::Float,
16654            "text" => ColumnTypeName::Text,
16655            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16656            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16657            // real MySQL schema and NONE of them existed: the CREATE
16658            // failed outright with `type "blob" does not exist`, so the
16659            // table was never made. The sizes differ only in MySQL's
16660            // maximum length, which SPG does not cap, so they collapse
16661            // onto TEXT and BYTEA the way the unsized spellings do.
16662            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16663            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16664            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16665            // enforce, consumed so the declaration parses.
16666            "varbinary" | "binary" => {
16667                self.consume_optional_paren_size();
16668                ColumnTypeName::Bytes
16669            }
16670            "name" => ColumnTypeName::Name,
16671            "xid" => ColumnTypeName::Xid,
16672            "oid" => ColumnTypeName::Oid,
16673            "xid8" => ColumnTypeName::Xid8,
16674            "bool" | "boolean" => ColumnTypeName::Bool,
16675            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16676            // an unbounded `character varying`, which the arm below has always
16677            // read as text. Only the short spelling demanded a length, so
16678            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16679            // there is — failed on `VARCHAR type requires (N)` while the long
16680            // spelling of the same thing was accepted. The same asymmetry
16681            // round 613 closed on the CAST side, here on the DDL side.
16682            "varchar" => {
16683                if matches!(self.peek(), Token::LParen) {
16684                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16685                } else {
16686                    ColumnTypeName::Text
16687                }
16688            }
16689            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16690            // `character` below (SQL standard).
16691            "char" => {
16692                if matches!(self.peek(), Token::LParen) {
16693                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16694                } else {
16695                    ColumnTypeName::Char(1)
16696                }
16697            }
16698            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16699            // `character(n)` = char, bare `character` = char(1). Unbounded
16700            // `character varying` maps to text.
16701            "character" => {
16702                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16703                    self.advance();
16704                    if matches!(self.peek(), Token::LParen) {
16705                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16706                    } else {
16707                        ColumnTypeName::Text
16708                    }
16709                } else if matches!(self.peek(), Token::LParen) {
16710                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16711                } else {
16712                    ColumnTypeName::Char(1)
16713                }
16714            }
16715            "vector" => {
16716                let dim = self.parse_paren_size("VECTOR")?;
16717                let encoding = self.parse_optional_vector_encoding()?;
16718                ColumnTypeName::Vector { dim, encoding }
16719            }
16720            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16721            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16722            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16723            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16724            // DECIMAL(10,2))` — how nearly every money column is written,
16725            // in either dialect — was a syntax error and the table was
16726            // never created. `FIXED` is MySQL's alias alone, so it is
16727            // taken only in that dialect.
16728            "numeric" | "decimal" | "dec" => {
16729                let (precision, scale) = self.parse_optional_numeric_params()?;
16730                ColumnTypeName::Numeric(precision, scale)
16731            }
16732            "fixed" if self.mysql_dialect => {
16733                let (precision, scale) = self.parse_optional_numeric_params()?;
16734                ColumnTypeName::Numeric(precision, scale)
16735            }
16736            "date" => ColumnTypeName::Date,
16737            // MySQL's `DATETIME` is the same domain as standard
16738            // `TIMESTAMP` — accept both spellings.
16739            "timestamp" | "datetime" => {
16740                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16741                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16742                // TIME ZONE` clause, so consume it first.
16743                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16744                // (it truncates on write and pads on render), so capture it;
16745                // a bare spelling means precision 0 there. PG stores µs always
16746                // and keeps `None`.
16747                let n = self.take_optional_paren_size();
16748                if self.mysql_dialect {
16749                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16750                }
16751                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16752                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16753                // the full form. SPG canonicalises:
16754                //   - WITH TIME ZONE    → Timestamptz
16755                //   - WITHOUT TIME ZONE → Timestamp
16756                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16757                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16758                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16759                {
16760                    self.advance(); // WITH
16761                    self.advance(); // TIME
16762                    self.advance(); // ZONE
16763                    ColumnTypeName::Timestamptz
16764                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16765                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16766                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16767                {
16768                    self.advance(); // WITHOUT
16769                    self.advance(); // TIME
16770                    self.advance(); // ZONE
16771                    ColumnTypeName::Timestamp
16772                } else {
16773                    // A second `(precision)` cannot legally follow, but the
16774                    // old grammar tolerated it; keep that tolerance.
16775                    self.consume_optional_paren_size();
16776                    ColumnTypeName::Timestamp
16777                }
16778            }
16779            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16780            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16781            // only PG-wire OID differs.
16782            "timestamptz" => {
16783                self.consume_optional_paren_size();
16784                ColumnTypeName::Timestamptz
16785            }
16786            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16787            // validation. We accept the JSONB spelling too because
16788            // most PG clients default to it; SPG doesn't distinguish
16789            // the two (no path-operator perf advantage to model).
16790            "json" => ColumnTypeName::Json,
16791            "jsonb" => ColumnTypeName::Jsonb,
16792            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16793            // surface here. Same storage shape; mapping happens at
16794            // the engine side via the ColumnTypeName → DataType
16795            // resolver. Literal forms are handled at coerce_value
16796            // time so the lexer stays untouched.
16797            "bytea" | "bytes" => ColumnTypeName::Bytes,
16798            // v7.17.0 Phase 7 — PG network address types
16799            // v7.17.0 had a Text-backed fallback here for
16800            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16801            // each to a first-class type; the keywords are
16802            // bound below in the ζ-A block.
16803            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16804            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16805            // arrives in v7.12.1+; the type itself loads here so
16806            // mailrs's `scripts/init-schema.sql` runs unmodified.
16807            "tsvector" => ColumnTypeName::TsVector,
16808            "tsquery" => ColumnTypeName::TsQuery,
16809            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16810            // surface for Django / Rails / Hibernate's default
16811            // PK pattern.
16812            "uuid" => ColumnTypeName::Uuid,
16813            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16814            // Storage = three-field {months, days, micros}, catalog
16815            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16816            // line `INTERVAL` was parser-rejected at CREATE TABLE.
16817            "interval" => {
16818                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16819                // SECOND` and an optional `(p)` precision. SPG stores the full
16820                // {months,days,micros}; consume + ignore the qualifier/precision.
16821                while matches!(self.peek(), Token::To)
16822                    || matches!(self.peek(), Token::Ident(s) if matches!(
16823                        s.to_ascii_lowercase().as_str(),
16824                        "year" | "month" | "day" | "hour" | "minute" | "second"
16825                    ))
16826                {
16827                    self.advance();
16828                }
16829                self.consume_optional_paren_size();
16830                ColumnTypeName::Interval
16831            }
16832            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16833            // i64 microseconds since 00:00:00. Wire OID 1083.
16834            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16835            "time" => {
16836                // v7.39 (round 424) — MySQL TIME carries a semantic
16837                // fractional-seconds precision, bare meaning 0.
16838                let n = self.take_optional_paren_size();
16839                if self.mysql_dialect {
16840                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16841                }
16842                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16843                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16844                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16845                {
16846                    self.advance();
16847                    self.advance();
16848                    self.advance();
16849                    ColumnTypeName::TimeTz
16850                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16851                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16852                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16853                {
16854                    self.advance();
16855                    self.advance();
16856                    self.advance();
16857                    ColumnTypeName::Time
16858                } else {
16859                    ColumnTypeName::Time
16860                }
16861            }
16862            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16863            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16864            "year" => ColumnTypeName::Year,
16865            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16866            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16867            "timetz" => ColumnTypeName::TimeTz,
16868            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16869            // Wire OID 790.
16870            "money" => ColumnTypeName::Money,
16871            // v7.17.0 Phase 3.P0-38 — PG range types.
16872            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16873            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16874            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16875            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16876            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16877            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16878            // v7.37.5 δ — PG 14+ multirange keywords.
16879            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16880            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16881            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16882            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16883            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16884            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16885            // v7.37.5 ε — PG geometry scalar keywords.
16886            "point" => ColumnTypeName::Point,
16887            "lseg" => ColumnTypeName::Lseg,
16888            "path" => ColumnTypeName::Path,
16889            "box" => ColumnTypeName::PgBox,
16890            "polygon" => ColumnTypeName::Polygon,
16891            "line" => ColumnTypeName::Line,
16892            "circle" => ColumnTypeName::Circle,
16893            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16894            "inet" => ColumnTypeName::Inet,
16895            "cidr" => ColumnTypeName::Cidr,
16896            "macaddr" => ColumnTypeName::Macaddr,
16897            "macaddr8" => ColumnTypeName::Macaddr8,
16898            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16899            // width in the value, so the optional `(N)` typmod is accepted and
16900            // ignored (the column stores whatever width it's given).
16901            "bit" => {
16902                let varying = matches!(
16903                    self.peek(),
16904                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16905                );
16906                if varying {
16907                    self.advance();
16908                }
16909                // v7.39 (round 281) — the length used to be parsed and
16910                // dropped, so `bit(3)` accepted a five-bit string.
16911                let n = if matches!(self.peek(), Token::LParen) {
16912                    self.parse_paren_size("BIT")?
16913                } else {
16914                    0
16915                };
16916                if varying {
16917                    ColumnTypeName::BitVarying(n)
16918                } else {
16919                    ColumnTypeName::Bit(n)
16920                }
16921            }
16922            "varbit" => {
16923                let n = if matches!(self.peek(), Token::LParen) {
16924                    self.parse_paren_size("VARBIT")?
16925                } else {
16926                    0
16927                };
16928                ColumnTypeName::BitVarying(n)
16929            }
16930            "xml" => ColumnTypeName::Xml,
16931            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16932            "hstore" => ColumnTypeName::Hstore,
16933            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16934            // `ENUM('a','b','c')`. Storage is TEXT; the value
16935            // list lands on `inline_enum_variants` for the
16936            // engine to validate INSERT cells against. Empty
16937            // value list is a parse error (matches MySQL).
16938            "enum" => {
16939                // Expect the opening `(`.
16940                if !matches!(self.peek(), Token::LParen) {
16941                    return Err(self.err(alloc::format!(
16942                        "expected '(' after ENUM, got {:?}",
16943                        self.peek()
16944                    )));
16945                }
16946                self.advance();
16947                let mut variants: Vec<String> = Vec::new();
16948                loop {
16949                    match self.advance() {
16950                        Token::String(s) => variants.push(s),
16951                        other => {
16952                            return Err(self.err(alloc::format!(
16953                                "ENUM(...) expects string literal variants, got {other:?}"
16954                            )));
16955                        }
16956                    }
16957                    match self.peek() {
16958                        Token::Comma => {
16959                            self.advance();
16960                            continue;
16961                        }
16962                        Token::RParen => {
16963                            self.advance();
16964                            break;
16965                        }
16966                        other => {
16967                            return Err(self.err(alloc::format!(
16968                                "expected ',' or ')' in ENUM(...), got {other:?}"
16969                            )));
16970                        }
16971                    }
16972                }
16973                if variants.is_empty() {
16974                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
16975                }
16976                inline_enum_variants = Some(variants);
16977                // Storage is plain TEXT; the variant list lives on
16978                // the ColumnSchema side.
16979                ColumnTypeName::Text
16980            }
16981            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16982            // `SET('a','b','c')`. Same parse shape as ENUM;
16983            // semantics differ (subset rather than pick-one).
16984            "set" => {
16985                if !matches!(self.peek(), Token::LParen) {
16986                    return Err(self.err(alloc::format!(
16987                        "expected '(' after SET, got {:?}",
16988                        self.peek()
16989                    )));
16990                }
16991                self.advance();
16992                let mut variants: Vec<String> = Vec::new();
16993                loop {
16994                    match self.advance() {
16995                        Token::String(s) => variants.push(s),
16996                        other => {
16997                            return Err(self.err(alloc::format!(
16998                                "SET(...) expects string literal variants, got {other:?}"
16999                            )));
17000                        }
17001                    }
17002                    match self.peek() {
17003                        Token::Comma => {
17004                            self.advance();
17005                            continue;
17006                        }
17007                        Token::RParen => {
17008                            self.advance();
17009                            break;
17010                        }
17011                        other => {
17012                            return Err(self.err(alloc::format!(
17013                                "expected ',' or ')' in SET(...), got {other:?}"
17014                            )));
17015                        }
17016                    }
17017                }
17018                if variants.is_empty() {
17019                    return Err(self.err("SET(...) must declare at least one variant".into()));
17020                }
17021                inline_set_variants = Some(variants);
17022                ColumnTypeName::Text
17023            }
17024            _other => {
17025                // v7.17.0 Phase 1.4 — unknown ident → defer
17026                // resolution to the engine. Stored as Text in
17027                // ColumnTypeName + the original name carried as
17028                // `user_type_ref` so CREATE TABLE can look up
17029                // user-defined enum / domain types.
17030                user_type_ref = Some(ty_ident.clone());
17031                ColumnTypeName::Text
17032            }
17033        };
17034        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17035        // right after the type keyword. Pre-4.4 SPG consumed +
17036        // discarded the keyword, leaving a customer column
17037        // declared `id INT UNSIGNED NOT NULL` silently accepting
17038        // negative values — a Tier-A correctness drift where
17039        // application invariants (auto-increment-IDs never
17040        // negative) silently broke on cutover. Now: capture as
17041        // a column flag, persist on the schema, enforce at
17042        // INSERT / UPDATE time.
17043        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17044        {
17045            self.advance();
17046            true
17047        } else {
17048            false
17049        };
17050        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17051        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17052        // stores text as UTF-8 always so CHARACTER SET is still a
17053        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17054        // name: it gets classified into a `Collation` variant the
17055        // engine consults at WHERE-eval time. PG `default` /
17056        // `pg_catalog.default` / `C` / `POSIX` collations all
17057        // resolve to `Binary` (the prior behaviour); `_ci` /
17058        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17059        // The schema-qualifier form (`pg_catalog.default`) lexes
17060        // as `Ident '.' Ident` — peek for the `.` and consume both
17061        // halves so it's treated as one collation name. PG's
17062        // `IDENT.IDENT` collation form (which can appear here) is
17063        // resolved by Collation::from_collation_name on the bare
17064        // identifier after the dot.
17065        let mut collation = Collation::Binary;
17066        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17067        // clause was written. The engine needs this to tell an explicit
17068        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17069        // clause at all: both resolve to `Collation::Binary`, but under the
17070        // MySQL dialect the latter takes the folding default collation.
17071        let mut collation_explicit = false;
17072        let mut collation_name: Option<alloc::string::String> = None;
17073        loop {
17074            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17075                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17076            {
17077                self.advance(); // CHARACTER
17078                self.advance(); // SET
17079                if matches!(
17080                    self.peek(),
17081                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17082                ) {
17083                    self.advance();
17084                }
17085                continue;
17086            }
17087            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17088                self.advance(); // COLLATE
17089                // Accept Ident / QuotedIdent / String AND the
17090                // keyword-tokenised `Default` (PG `pg_catalog.default`
17091                // and bare `DEFAULT` collation names — `default` is a
17092                // reserved word so the lexer hands back Token::Default
17093                // not Token::Ident).
17094                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17095                    match this.peek().clone() {
17096                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17097                            this.advance();
17098                            Some(s)
17099                        }
17100                        Token::Default => {
17101                            this.advance();
17102                            Some(alloc::string::String::from("default"))
17103                        }
17104                        _ => None,
17105                    }
17106                };
17107                let raw = if let Some(head) = read_collation_atom(self) {
17108                    // Schema-qualified PG form: `pg_catalog.default`.
17109                    if matches!(self.peek(), Token::Dot) {
17110                        self.advance();
17111                        let tail = read_collation_atom(self).unwrap_or_default();
17112                        alloc::format!("{head}.{tail}")
17113                    } else {
17114                        head
17115                    }
17116                } else {
17117                    alloc::string::String::new()
17118                };
17119                if !raw.is_empty() {
17120                    collation_explicit = true;
17121                    // v7.39 (round 676) — keep the name too. The enum below
17122                    // folds C / POSIX / en_US / default into one value, and
17123                    // `pg_attribute.attcollation` has to tell them apart.
17124                    // The schema qualifier goes: PG's `pg_catalog.default`
17125                    // and a bare `default` name the same collation.
17126                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17127                    // encoding suffix. Round 676 used `rsplit('.')` for
17128                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17129                    // PG writes `pg_catalog.default` (qualifier) and
17130                    // `en_US.utf8` (locale + encoding) with the same
17131                    // separator. Only `pg_catalog.` is a qualifier, and it
17132                    // is the only one PG's own dumps emit.
17133                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17134                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17135                    collation_name = Some(alloc::string::String::from(bare));
17136                    let parsed = Collation::from_collation_name(&raw);
17137                    // Last COLLATE clause wins, but `Binary` from a
17138                    // bare keyword like `default` should not
17139                    // silently downgrade a stronger one set earlier
17140                    // on the same column. v7.17 only ships one
17141                    // non-Binary variant so a simple OR is enough.
17142                    if parsed != Collation::Binary {
17143                        collation = parsed;
17144                    }
17145                }
17146                continue;
17147            }
17148            break;
17149        }
17150        // v7.10.10 — postfix `[]` widens the base type to its array
17151        // type. PG accepts `TYPE[]` after any base type and so does
17152        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17153        // all through; the old "only TEXT[]" note was stale).
17154        if matches!(self.peek(), Token::LBracket) {
17155            self.advance();
17156            if !matches!(self.peek(), Token::RBracket) {
17157                return Err(self.err(alloc::format!(
17158                    "TEXT[] takes no dimension; got {:?}",
17159                    self.peek()
17160                )));
17161            }
17162            self.advance();
17163            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17164            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17165            // still error here.
17166            ty = match ty {
17167                ColumnTypeName::Text => ColumnTypeName::TextArray,
17168                ColumnTypeName::Int => ColumnTypeName::IntArray,
17169                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17170                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17171                // `[]` grammar. Wire OID 1187.
17172                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17173                // v7.37.5 γ — full PG array-of-scalar family.
17174                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17175                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17176                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17177                // NUMERIC(p, s) loses its precision params at the
17178                // array level (matches PG: `NUMERIC[]` is untyped,
17179                // per-element precision flows through values).
17180                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17181                ColumnTypeName::Date => ColumnTypeName::DateArray,
17182                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17183                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17184                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17185                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17186                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17187                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17188                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17189                // the array level (matches PG semantics where the
17190                // element precision is per-row, not column-wide).
17191                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17192                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17193                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17194                // follow-up.
17195                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17196                other => {
17197                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17198                }
17199            };
17200            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17201            // for INT/TEXT/BIGINT. Anything else is an error.
17202            if matches!(self.peek(), Token::LBracket) {
17203                self.advance();
17204                if !matches!(self.peek(), Token::RBracket) {
17205                    return Err(self.err(alloc::format!(
17206                        "TYPE[][] second dimension takes no size; got {:?}",
17207                        self.peek()
17208                    )));
17209                }
17210                self.advance();
17211                ty = match ty {
17212                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17213                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17214                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17215                    // v7.39 (read01 round 75) — bool[][].
17216                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17217                    other => {
17218                        return Err(self.err(alloc::format!(
17219                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17220                             TEXT[][] only; got {other:?}"
17221                        )));
17222                    }
17223                };
17224            }
17225        }
17226        Ok((
17227            ty,
17228            implied_auto_increment,
17229            implied_not_null,
17230            user_type_ref,
17231            collation,
17232            collation_explicit,
17233            collation_name,
17234            is_unsigned,
17235            inline_enum_variants,
17236            inline_set_variants,
17237            mysql_int_width,
17238            mysql_fsp,
17239        ))
17240    }
17241
17242    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17243        // v7.20 — PG reserves the table-constraint keywords, so a
17244        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17245        // malformed constraint clause (e.g. `UNIQUE a` missing its
17246        // parens), not a column named "unique". Since v7.17's
17247        // unknown-type leniency (`user_type_ref`) such a clause
17248        // would otherwise parse as a column with a user-defined
17249        // type — silently accepting invalid DDL. Quoted
17250        // identifiers ("unique" / `unique`) remain valid names.
17251        if let Token::Ident(s) = self.peek()
17252            && [
17253                "unique",
17254                "primary",
17255                "foreign",
17256                "constraint",
17257                "check",
17258                "references",
17259                "exclude",
17260            ]
17261            .iter()
17262            .any(|kw| s.eq_ignore_ascii_case(kw))
17263        {
17264            return Err(self.err(alloc::format!(
17265                "unexpected reserved keyword '{s}' at start of column definition \
17266                 (malformed table constraint?)"
17267            )));
17268        }
17269        let name = self.expect_ident_like()?;
17270        let (
17271            ty,
17272            implied_auto_increment,
17273            implied_not_null,
17274            user_type_ref,
17275            collation,
17276            collation_explicit,
17277            collation_name,
17278            is_unsigned,
17279            inline_enum_variants,
17280            inline_set_variants,
17281            mysql_int_width,
17282            mysql_fsp,
17283        ) = self.parse_type_with_implied_flags()?;
17284        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17285        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17286        // each at most once.
17287        let mut default: Option<Expr> = None;
17288        let mut nullable = !implied_not_null;
17289        let mut nullability_seen = implied_not_null;
17290        let mut auto_increment = implied_auto_increment;
17291        let mut is_primary_key = false;
17292        let mut is_unique = false;
17293        let mut unique_nulls_not_distinct = false;
17294        let mut constraint_deferrable = false;
17295        let mut constraint_initially_deferred = false;
17296        let mut check: Option<Expr> = None;
17297        let mut on_update_runtime: Option<Expr> = None;
17298        let mut generated_stored_expr: Option<Box<Expr>> = None;
17299        let mut identity_always = false;
17300        loop {
17301            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17302            // not-null constraints by name and pg_dump emits them
17303            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17304            // NOT NULL`. Accept and discard the name; whatever
17305            // constraint follows is parsed by the arms below.
17306            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17307                // v7.39 (round 308, V29) — a name on an inline
17308                // REFERENCES belongs to the FOREIGN KEY, and the caller
17309                // (`parse_column_def_with_fk`) is what builds it, so
17310                // leave the whole clause for it. Dropping the name here
17311                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17312                // as the synthesised `c_pid_fkey` — which then could
17313                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17314                // `advance()` takes tokens by `mem::replace`, so there
17315                // is no rewinding once consumed.
17316                if matches!(
17317                    self.tokens.get(self.pos + 2),
17318                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17319                ) {
17320                    break;
17321                }
17322                self.advance();
17323                let _name = self.expect_ident_like()?;
17324                continue;
17325            }
17326            // v7.39 (round 379) — MySQL's SHORT generated-column form
17327            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17328            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17329            // below), but hand-written schemas and app migrations use this.
17330            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17331            // SPG computes-and-stores either way, like the long form.
17332            if matches!(self.peek(), Token::As) {
17333                self.advance();
17334                if !matches!(self.peek(), Token::LParen) {
17335                    return Err(self.err(alloc::format!(
17336                        "expected '(' after AS in a generated column, got {:?}",
17337                        self.peek()
17338                    )));
17339                }
17340                self.advance();
17341                let expr = self.parse_expr(0)?;
17342                if !matches!(self.peek(), Token::RParen) {
17343                    return Err(self.err(alloc::format!(
17344                        "expected ')' after AS (<expr>), got {:?}",
17345                        self.peek()
17346                    )));
17347                }
17348                self.advance();
17349                if matches!(self.peek(), Token::Ident(s)
17350                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17351                {
17352                    self.advance();
17353                }
17354                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17355                continue;
17356            }
17357            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17358            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17359            // the modern replacement for SERIAL in hand-written
17360            // schemas). Both flavours map onto the auto-increment
17361            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17362            // ALWAYS's reject-explicit-values nuance is documented
17363            // leniency. Generated EXPRESSION columns
17364            // (`AS (expr) STORED`) are not supported: error loudly
17365            // instead of silently storing NULLs.
17366            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17367                self.advance();
17368                let mut saw_generated_always = false;
17369                match self.peek().clone() {
17370                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17371                        self.advance();
17372                        saw_generated_always = true;
17373                    }
17374                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17375                        self.advance();
17376                        if !matches!(self.peek(), Token::Default) {
17377                            return Err(self.err(alloc::format!(
17378                                "expected DEFAULT after GENERATED BY, got {:?}",
17379                                self.peek()
17380                            )));
17381                        }
17382                        self.advance();
17383                    }
17384                    other => {
17385                        return Err(self.err(alloc::format!(
17386                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17387                        )));
17388                    }
17389                }
17390                if !matches!(self.peek(), Token::As) {
17391                    return Err(self.err(alloc::format!(
17392                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17393                        self.peek()
17394                    )));
17395                }
17396                self.advance();
17397                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17398                // ( <expr> ) STORED` stored computed-column. The
17399                // expression is captured for the engine to recompute
17400                // on every INSERT / UPDATE. v7.37.7 accepts the
17401                // STORED keyword only; PG also has VIRTUAL, which
17402                // v7.37.7 carves out (sentori only uses STORED).
17403                if matches!(self.peek(), Token::LParen) {
17404                    self.advance();
17405                    let expr = self.parse_expr(0)?;
17406                    if !matches!(self.peek(), Token::RParen) {
17407                        return Err(self.err(alloc::format!(
17408                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17409                            self.peek()
17410                        )));
17411                    }
17412                    self.advance();
17413                    let stored = match self.peek() {
17414                        Token::Ident(s) | Token::QuotedIdent(s)
17415                            if s.eq_ignore_ascii_case("stored") =>
17416                        {
17417                            self.advance();
17418                            true
17419                        }
17420                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17421                        // generated columns. SPG computes them on write and
17422                        // persists like STORED; the two are observably
17423                        // identical for query results (the value, recompute
17424                        // on base-column change, and NOT NULL enforcement all
17425                        // match), so a PG 18 schema/dump using VIRTUAL loads
17426                        // and behaves correctly. The compute-on-read storage
17427                        // saving is an invisible internal difference.
17428                        Token::Ident(s) | Token::QuotedIdent(s)
17429                            if s.eq_ignore_ascii_case("virtual") =>
17430                        {
17431                            self.advance();
17432                            false
17433                        }
17434                        other => {
17435                            return Err(self.err(alloc::format!(
17436                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17437                                 got {other:?}"
17438                            )));
17439                        }
17440                    };
17441                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17442                    generated_stored_expr = Some(Box::new(expr));
17443                    continue;
17444                }
17445                self.expect_keyword_ident("identity")?;
17446                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17447                // consume the balanced parens and discard (SPG's
17448                // auto-increment is max+1-scan based).
17449                if matches!(self.peek(), Token::LParen) {
17450                    let mut depth = 0usize;
17451                    loop {
17452                        match self.advance() {
17453                            Token::LParen => depth += 1,
17454                            Token::RParen => {
17455                                depth -= 1;
17456                                if depth == 0 {
17457                                    break;
17458                                }
17459                            }
17460                            Token::Eof => {
17461                                return Err(self.err(
17462                                    "unterminated sequence-options parens after IDENTITY".into(),
17463                                ));
17464                            }
17465                            _ => {}
17466                        }
17467                    }
17468                }
17469                auto_increment = true;
17470                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17471                // can reject explicit non-DEFAULT INSERT values (unless
17472                // OVERRIDING SYSTEM VALUE) the way PG does.
17473                identity_always = saw_generated_always;
17474                // PG identity columns are implicitly NOT NULL.
17475                nullable = false;
17476                continue;
17477            }
17478            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17479            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17480            // is accepted today. The "ON" token is an Ident
17481            // (not reserved) — peek before consuming.
17482            if matches!(self.peek(), Token::On)
17483                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17484            {
17485                self.advance(); // ON
17486                self.advance(); // update
17487                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17488                let next = self.peek().clone();
17489                match next {
17490                    Token::Ident(s) | Token::QuotedIdent(s)
17491                        if s.eq_ignore_ascii_case("current_timestamp") =>
17492                    {
17493                        self.advance();
17494                        // Optional `(N)` precision.
17495                        if matches!(self.peek(), Token::LParen) {
17496                            self.advance();
17497                            if !matches!(self.peek(), Token::Integer(_)) {
17498                                return Err(self.err(alloc::format!(
17499                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17500                                    self.peek()
17501                                )));
17502                            }
17503                            self.advance();
17504                            if !matches!(self.peek(), Token::RParen) {
17505                                return Err(self.err(alloc::format!(
17506                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17507                                    self.peek()
17508                                )));
17509                            }
17510                            self.advance();
17511                        }
17512                        on_update_runtime = Some(Expr::FunctionCall {
17513                            name: "now".into(),
17514                            args: Vec::new(),
17515                        });
17516                        continue;
17517                    }
17518                    other => {
17519                        return Err(self.err(alloc::format!(
17520                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17521                        )));
17522                    }
17523                }
17524            }
17525            if matches!(self.peek(), Token::Default) {
17526                if default.is_some() {
17527                    return Err(self.err("DEFAULT specified twice".into()));
17528                }
17529                self.advance();
17530                default = Some(self.parse_expr(0)?);
17531                continue;
17532            }
17533            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17534            // token with NOT NULL and sits EARLIER in the loop than the
17535            // deferrability arm, so without the lookahead it was reported as
17536            // "NOT NULL specified twice" (or "expected NULL after NOT").
17537            if matches!(self.peek(), Token::Not)
17538                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17539            {
17540                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17541                self.consume_optional_deferrable_clauses()?;
17542                continue;
17543            }
17544            if matches!(self.peek(), Token::Not) {
17545                if nullability_seen {
17546                    return Err(self.err("NOT NULL specified twice".into()));
17547                }
17548                self.advance();
17549                if !matches!(self.peek(), Token::Null) {
17550                    return Err(self.err(format!(
17551                        "expected NULL after NOT in column def, got {:?}",
17552                        self.peek()
17553                    )));
17554                }
17555                self.advance();
17556                nullable = false;
17557                nullability_seen = true;
17558                continue;
17559            }
17560            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17561            // "this column is nullable" marker (the default in
17562            // standard SQL anyway). mysqldump emits it routinely
17563            // (`col TYPE NULL DEFAULT NULL` for nullable
17564            // timestamps etc). Accept + no-op.
17565            if matches!(self.peek(), Token::Null) {
17566                if nullability_seen && !nullable {
17567                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17568                    // sentence, PG18-measured (the table name is the
17569                    // caller's; the column half is exact).
17570                    return Err(self.err(alloc::format!(
17571                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17572                    )));
17573                }
17574                self.advance();
17575                nullable = true;
17576                nullability_seen = true;
17577                continue;
17578            }
17579            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17580            // arrives as a bare Ident. Match either, case-insensitive.
17581            if let Token::Ident(s) = self.peek()
17582                && (s.eq_ignore_ascii_case("auto_increment")
17583                    || s.eq_ignore_ascii_case("autoincrement"))
17584            {
17585                if auto_increment {
17586                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17587                }
17588                self.advance();
17589                auto_increment = true;
17590                continue;
17591            }
17592            // v7.9.13 — inline `PRIMARY KEY` column constraint
17593            // (mailrs F1). Implies `NOT NULL`. The engine creates
17594            // a BTree index for the PK column at CREATE TABLE time
17595            // so FK parent-side index lookups resolve.
17596            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17597            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17598            // spelling was a parse error, so a pg_dump carrying one stopped
17599            // mid-restore. The clauses are consumed by the same helper the FK
17600            // path has used since round 288 and recorded nowhere: SPG enforces
17601            // the constraint IMMEDIATELY either way, which fails earlier than
17602            // PG inside a transaction that violates-then-repairs — a refusal,
17603            // not a wrong answer. True deferral is the open remainder of F08.
17604            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17605                || (matches!(self.peek(), Token::Not)
17606                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17607            {
17608                // v7.39 (round 711) — CARRIED now (the storing half of
17609                // F08); round 621 only consumed.
17610                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17611                constraint_deferrable |= d;
17612                constraint_initially_deferred |= idef;
17613                continue;
17614            }
17615            if let Token::Ident(s) = self.peek()
17616                && s.eq_ignore_ascii_case("primary")
17617            {
17618                if is_primary_key {
17619                    return Err(self.err("PRIMARY KEY specified twice".into()));
17620                }
17621                // Peek-ahead for the required `KEY` token.
17622                let next = self.tokens.get(self.pos + 1);
17623                let next_is_key = matches!(
17624                    next,
17625                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17626                );
17627                if !next_is_key {
17628                    return Err(self.err(format!(
17629                        "expected KEY after PRIMARY in column def, got {:?}",
17630                        next
17631                    )));
17632                }
17633                self.advance(); // PRIMARY
17634                self.advance(); // KEY
17635                is_primary_key = true;
17636                if nullability_seen && nullable {
17637                    return Err(self.err(
17638                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17639                    ));
17640                }
17641                nullable = false;
17642                nullability_seen = true;
17643                continue;
17644            }
17645            // v7.13.0 — inline `UNIQUE` column constraint
17646            // (mailrs round-5 G2). Fold into a single-column
17647            // table-level UNIQUE at CREATE TABLE post-process time.
17648            if let Token::Ident(s) = self.peek()
17649                && s.eq_ignore_ascii_case("unique")
17650            {
17651                if is_unique {
17652                    return Err(self.err("UNIQUE specified twice".into()));
17653                }
17654                self.advance();
17655                is_unique = true;
17656                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17657                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17658                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17659                    let n1 = self.tokens.get(self.pos + 1);
17660                    let n2 = self.tokens.get(self.pos + 2);
17661                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17662                        self.advance(); // NULLS
17663                        self.advance(); // NOT
17664                        self.advance(); // DISTINCT
17665                        unique_nulls_not_distinct = true;
17666                    } else if matches!(n1, Some(Token::Distinct)) {
17667                        self.advance(); // NULLS
17668                        self.advance(); // DISTINCT
17669                    }
17670                }
17671                continue;
17672            }
17673            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17674            // (mailrs round-5 G3). PG semantics: column-level
17675            // CHECK is equivalent to a table-level CHECK. Multiple
17676            // inline CHECKs on the same column AND together.
17677            if let Token::Ident(s) = self.peek()
17678                && s.eq_ignore_ascii_case("check")
17679            {
17680                self.advance();
17681                if !matches!(self.peek(), Token::LParen) {
17682                    return Err(self.err(alloc::format!(
17683                        "expected '(' after CHECK in column def, got {:?}",
17684                        self.peek()
17685                    )));
17686                }
17687                self.advance();
17688                let pred = self.parse_expr(0)?;
17689                if !matches!(self.peek(), Token::RParen) {
17690                    return Err(self.err(alloc::format!(
17691                        "expected ')' to close CHECK predicate, got {:?}",
17692                        self.peek()
17693                    )));
17694                }
17695                self.advance();
17696                check = Some(match check.take() {
17697                    Some(prev) => Expr::Binary {
17698                        op: BinOp::And,
17699                        lhs: Box::new(prev),
17700                        rhs: Box::new(pred),
17701                    },
17702                    None => pred,
17703                });
17704                continue;
17705            }
17706            break;
17707        }
17708        Ok(ColumnDef {
17709            name,
17710            ty,
17711            nullable,
17712            default,
17713            auto_increment,
17714            is_primary_key,
17715            is_unique,
17716            unique_nulls_not_distinct,
17717            constraint_deferrable,
17718            constraint_initially_deferred,
17719            check,
17720            user_type_ref,
17721            on_update_runtime,
17722            collation,
17723            collation_explicit,
17724            collation_name,
17725            is_unsigned,
17726            inline_enum_variants,
17727            inline_set_variants,
17728            generated_stored_expr,
17729            identity_always,
17730            mysql_int_width,
17731            mysql_fsp,
17732        })
17733    }
17734
17735    /// `NUMERIC` may appear without parameters, with one (precision
17736    /// only, scale=0), or with both. Returns `(precision, scale)` with
17737    /// 0 = unspecified for the bare form.
17738    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17739        if !matches!(self.peek(), Token::LParen) {
17740            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17741            // we surface it as precision=0 to mean "unconstrained" so
17742            // the engine doesn't need a separate variant.
17743            return Ok((0, 0));
17744        }
17745        self.advance();
17746        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17747        // it words the out-of-range case with the value it saw. SPG
17748        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17749        // accepts failed to parse at all; values wider than i128 are
17750        // carried by the arbitrary-precision form.
17751        let precision = match self.advance() {
17752            Token::Integer(n) if (1..=1000).contains(&n) => {
17753                u16::try_from(n).expect("range-checked")
17754            }
17755            Token::Integer(n) => {
17756                return Err(ParseError {
17757                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17758                    token_pos: self.consumed_pos(),
17759                });
17760            }
17761            other => {
17762                return Err(ParseError {
17763                    message: format!(
17764                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17765                    ),
17766                    token_pos: self.consumed_pos(),
17767                });
17768            }
17769        };
17770        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17771        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17772        // then overflows). A negative scale rounds to tens / hundreds / …
17773        let scale = if matches!(self.peek(), Token::Comma) {
17774            self.advance();
17775            let neg = if matches!(self.peek(), Token::Minus) {
17776                self.advance();
17777                true
17778            } else {
17779                false
17780            };
17781            match self.advance() {
17782                Token::Integer(n) => {
17783                    let signed = if neg { -n } else { n };
17784                    if !(-1000..=1000).contains(&signed) {
17785                        return Err(ParseError {
17786                            message: format!(
17787                                "NUMERIC scale {signed} must be between -1000 and 1000"
17788                            ),
17789                            token_pos: self.consumed_pos(),
17790                        });
17791                    }
17792                    i16::try_from(signed).expect("range-checked")
17793                }
17794                other => {
17795                    return Err(ParseError {
17796                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
17797                        token_pos: self.consumed_pos(),
17798                    });
17799                }
17800            }
17801        } else {
17802            0
17803        };
17804        if !matches!(self.peek(), Token::RParen) {
17805            return Err(self.err(format!(
17806                "expected ')' to close NUMERIC params, got {:?}",
17807                self.peek()
17808            )));
17809        }
17810        self.advance();
17811        Ok((precision, scale))
17812    }
17813
17814    /// Parse `(N)` where `N` is a positive integer literal — used by the
17815    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17816    /// for the error message.
17817    /// v6.0.1: parse the optional `USING <encoding>` clause that
17818    /// follows `VECTOR(N)` in a column definition. Missing clause
17819    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17820    /// ident → `ParseError` listing the encodings recognised today.
17821    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17822        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17823            return Ok(VecEncoding::F32);
17824        }
17825        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17826        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17827        // consume the token when the very next token is a known
17828        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17829        // `USING` for the caller — it's the rewrite-expression form.
17830        let n1 = self.tokens.get(self.pos + 1);
17831        let next_is_encoding = matches!(
17832            n1,
17833            Some(Token::Ident(s))
17834                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17835        );
17836        if !next_is_encoding {
17837            return Ok(VecEncoding::F32);
17838        }
17839        self.advance();
17840        let enc_ident = match self.advance() {
17841            Token::Ident(s) => s,
17842            other => {
17843                return Err(self.err(format!(
17844                    "expected vector encoding after USING, got {other:?}"
17845                )));
17846            }
17847        };
17848        match enc_ident.to_ascii_lowercase().as_str() {
17849            "sq8" => Ok(VecEncoding::Sq8),
17850            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17851            // binary16 per-element storage.
17852            "half" => Ok(VecEncoding::F16),
17853            other => Err(self.err(format!(
17854                "unknown vector encoding {other:?}; supported: SQ8, HALF"
17855            ))),
17856        }
17857    }
17858
17859    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17860    /// without consuming it. Returns `Some(N)` when the next
17861    /// tokens are `( <int> )`; None otherwise. Used by the
17862    /// TINYINT classifier to decide whether to map to Bool or
17863    /// SmallInt.
17864    fn peek_optional_paren_size_value(&self) -> Option<i64> {
17865        if !matches!(self.peek(), Token::LParen) {
17866            return None;
17867        }
17868        let next = self.tokens.get(self.pos + 1)?;
17869        let n = match next {
17870            Token::Integer(n) => *n,
17871            _ => return None,
17872        };
17873        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17874            return None;
17875        }
17876        Some(n)
17877    }
17878
17879    /// v7.14.0 — consume an optional MySQL display-width
17880    /// parenthesised number after an integer type, returning
17881    /// nothing. `TINYINT(1)` etc.
17882    /// v7.39 (round 360) — does the parenthesised group ahead contain a
17883    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17884    fn peek_paren_has_comma(&self) -> bool {
17885        let mut i = self.pos + 1;
17886        let mut depth = 1usize;
17887        while depth > 0 {
17888            match self.tokens.get(i) {
17889                Some(Token::LParen) => depth += 1,
17890                Some(Token::RParen) => depth -= 1,
17891                Some(Token::Comma) if depth == 1 => return true,
17892                None | Some(Token::Eof) => return false,
17893                _ => {}
17894            }
17895            i += 1;
17896        }
17897        false
17898    }
17899
17900    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17901    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17902    /// fractional-seconds precision that drives write truncation and render
17903    /// padding, where `consume_optional_paren_size` throws it away.
17904    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17905    fn take_optional_paren_size(&mut self) -> Option<u8> {
17906        let Some(Token::Integer(n)) = self
17907            .tokens
17908            .get(self.pos + 1)
17909            .filter(|_| matches!(self.peek(), Token::LParen))
17910            .cloned()
17911        else {
17912            self.consume_optional_paren_size();
17913            return None;
17914        };
17915        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17916            self.consume_optional_paren_size();
17917            return None;
17918        }
17919        self.consume_optional_paren_size();
17920        u8::try_from(n).ok()
17921    }
17922
17923    fn consume_optional_paren_size(&mut self) {
17924        if !matches!(self.peek(), Token::LParen) {
17925            return;
17926        }
17927        self.advance();
17928        // Skip until matching RParen (allow nested or any tokens).
17929        let mut depth = 1usize;
17930        while depth > 0 {
17931            match self.peek() {
17932                Token::LParen => depth += 1,
17933                Token::RParen => depth -= 1,
17934                Token::Eof => return,
17935                _ => {}
17936            }
17937            self.advance();
17938        }
17939    }
17940
17941    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17942        if !matches!(self.peek(), Token::LParen) {
17943            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17944        }
17945        self.advance();
17946        let n = match self.advance() {
17947            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17948                message: format!("{label} size too large: {n}"),
17949                token_pos: self.consumed_pos(),
17950            })?,
17951            other => {
17952                return Err(ParseError {
17953                    message: format!("expected positive integer {label} size, got {other:?}"),
17954                    token_pos: self.consumed_pos(),
17955                });
17956            }
17957        };
17958        if !matches!(self.peek(), Token::RParen) {
17959            return Err(self.err(format!(
17960                "expected ')' after {label} size, got {:?}",
17961                self.peek()
17962            )));
17963        }
17964        self.advance();
17965        Ok(n)
17966    }
17967
17968    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17969    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17970    /// key, like MySQL) whose action skips conflicting rows.
17971    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17972    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17973    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17974    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17975    /// common bulk-upsert spellings —
17976    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17977    ///     REPLACE INTO t SELECT …
17978    /// — were a parse error / a duplicate-key failure respectively.
17979    ///
17980    /// Precedence: an explicitly written clause beats a statement-level flag.
17981    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17982    /// implicit `REPLACE` and `IGNORE` lowerings.
17983    fn parse_insert_conflict_clause(
17984        &mut self,
17985        replace: bool,
17986        ignore: bool,
17987    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17988        if let Some(c) = self.parse_optional_on_duplicate_key()? {
17989            return Ok(Some(c));
17990        }
17991        if let Some(c) = self.parse_optional_on_conflict()? {
17992            return Ok(Some(c));
17993        }
17994        if replace {
17995            // REPLACE INTO = delete-then-insert, which PG spells as
17996            // `ON CONFLICT DO UPDATE SET` over every column; the engine
17997            // reads an empty assignment list as "take the incoming row".
17998            return Ok(Some(crate::ast::OnConflictClause {
17999                target_columns: Vec::new(),
18000                index_where: None,
18001                constraint_name: None,
18002                mysql_lowered: true,
18003                action: crate::ast::OnConflictAction::Update {
18004                    assignments: Vec::new(),
18005                    where_: None,
18006                },
18007            }));
18008        }
18009        if ignore {
18010            return Ok(Some(Self::insert_ignore_clause()));
18011        }
18012        Ok(None)
18013    }
18014
18015    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18016    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18017    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18018    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18019    fn parse_optional_on_duplicate_key(
18020        &mut self,
18021    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18022        if !(matches!(self.peek(), Token::On)
18023            && matches!(self.tokens.get(self.pos + 1),
18024                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18025        {
18026            return Ok(None);
18027        }
18028        self.advance(); // ON
18029        self.advance(); // DUPLICATE
18030        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18031            return Err(self.err(format!(
18032                "expected KEY after ON DUPLICATE, got {:?}",
18033                self.peek()
18034            )));
18035        }
18036        self.advance();
18037        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18038            return Err(self.err(format!(
18039                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18040                self.peek()
18041            )));
18042        }
18043        self.advance();
18044        let mut assignments: Vec<(String, Expr)> = Vec::new();
18045        loop {
18046            let col = self.expect_ident_like()?;
18047            if !matches!(self.peek(), Token::Eq) {
18048                return Err(self.err(format!(
18049                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18050                    self.peek()
18051                )));
18052            }
18053            self.advance();
18054            let mut expr = self.parse_expr(0)?;
18055            Self::rewrite_mysql_values_refs(&mut expr);
18056            assignments.push((col, expr));
18057            if matches!(self.peek(), Token::Comma) {
18058                self.advance();
18059                continue;
18060            }
18061            break;
18062        }
18063        Ok(Some(crate::ast::OnConflictClause {
18064            target_columns: Vec::new(),
18065            index_where: None,
18066            constraint_name: None,
18067            mysql_lowered: true,
18068            action: crate::ast::OnConflictAction::Update {
18069                assignments,
18070                where_: None,
18071            },
18072        }))
18073    }
18074
18075    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18076        crate::ast::OnConflictClause {
18077            target_columns: Vec::new(),
18078            index_where: None,
18079            constraint_name: None,
18080            mysql_lowered: true,
18081            action: crate::ast::OnConflictAction::Nothing,
18082        }
18083    }
18084
18085    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18086        debug_assert!(
18087            matches!(self.peek(), Token::Insert)
18088                || (replace
18089                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18090        );
18091        self.advance();
18092        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18093        // would raise a duplicate-key error instead of failing the statement,
18094        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18095        // plain ident to the lexer; only the MySQL dialect accepts it here.
18096        let ignore = self.mysql_dialect
18097            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18098        if ignore {
18099            self.advance();
18100        }
18101        if !matches!(self.peek(), Token::Into) {
18102            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18103        }
18104        self.advance();
18105        let table = self.expect_ident_like()?;
18106        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18107        // grammar requires the AS keyword here (a bare identifier would be
18108        // ambiguous with a column list). The alias is what the ON CONFLICT
18109        // DO UPDATE expressions refer to the target row by.
18110        let alias = if matches!(self.peek(), Token::As) {
18111            self.advance();
18112            Some(self.expect_ident_like()?)
18113        } else {
18114            None
18115        };
18116        // v7.39 (round 428) — MySQL's SET-form INSERT:
18117        //     INSERT INTO t SET a = 1, b = 'x'
18118        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18119        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18120        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18121        // measured). So it lowers to the column list + one VALUES row and
18122        // rejoins the ordinary path, which already handles every one of
18123        // those. PG has no such spelling, hence the dialect gate.
18124        if self.mysql_dialect
18125            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18126        {
18127            self.advance(); // SET
18128            let mut names = Vec::new();
18129            let mut values = Vec::new();
18130            loop {
18131                names.push(self.expect_ident_like()?);
18132                if !matches!(self.peek(), Token::Eq) {
18133                    return Err(self.err(alloc::format!(
18134                        "expected '=' in INSERT … SET, got {:?}",
18135                        self.peek()
18136                    )));
18137                }
18138                self.advance();
18139                // `SET a = DEFAULT` rides the same `__column_default` marker
18140                // the VALUES-row and UPDATE-SET paths use; the INSERT
18141                // executor resolves it against the target column.
18142                if matches!(self.peek(), Token::Default) {
18143                    self.advance();
18144                    values.push(Expr::FunctionCall {
18145                        name: "__column_default".to_string(),
18146                        args: Vec::new(),
18147                    });
18148                } else {
18149                    values.push(self.parse_expr(0)?);
18150                }
18151                if matches!(self.peek(), Token::Comma) {
18152                    self.advance();
18153                    continue;
18154                }
18155                break;
18156            }
18157            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18158            let returning = self.parse_optional_returning()?;
18159            return Ok(Statement::Insert(InsertStatement {
18160                ctes: Vec::new(),
18161                table,
18162                alias,
18163                columns: Some(names),
18164                rows: alloc::vec![values],
18165                select_source: None,
18166                // MySQL's SET form has no `OVERRIDING …` clause (that is
18167                // PG's identity-column spelling).
18168                overriding: Overriding::None,
18169                mysql_ignore: ignore,
18170                on_conflict,
18171                returning,
18172            }));
18173        }
18174        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18175        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18176        // a parenthesized query source instead (PG select_with_parens:
18177        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18178        // both keywords are reserved in PG, so no column list can start
18179        // with them.
18180        let columns = if matches!(self.peek(), Token::LParen) {
18181            self.advance();
18182            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18183                let select_stmt = if self.peek_is_with_kw() {
18184                    self.advance();
18185                    self.parse_nested_with_select()?
18186                } else {
18187                    match self.parse_select_stmt()? {
18188                        Statement::Select(s) => s,
18189                        other => {
18190                            return Err(self.err(alloc::format!(
18191                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18192                            )));
18193                        }
18194                    }
18195                };
18196                if !matches!(self.peek(), Token::RParen) {
18197                    return Err(self.err(format!(
18198                        "expected ')' after parenthesized INSERT source, got {:?}",
18199                        self.peek()
18200                    )));
18201                }
18202                self.advance();
18203                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18204                let returning = self.parse_optional_returning()?;
18205                return Ok(Statement::Insert(InsertStatement {
18206                    ctes: Vec::new(),
18207                    table,
18208                    alias: alias.clone(),
18209                    columns: None,
18210                    rows: Vec::new(),
18211                    select_source: Some(Box::new(select_stmt)),
18212                    on_conflict,
18213                    returning,
18214                    overriding: Overriding::None,
18215                    mysql_ignore: ignore,
18216                }));
18217            }
18218            let mut names = Vec::new();
18219            loop {
18220                names.push(self.expect_ident_like()?);
18221                match self.peek() {
18222                    Token::Comma => {
18223                        self.advance();
18224                    }
18225                    Token::RParen => {
18226                        self.advance();
18227                        break;
18228                    }
18229                    other => {
18230                        return Err(self.err(format!(
18231                            "expected ',' or ')' in INSERT column list, got {other:?}"
18232                        )));
18233                    }
18234                }
18235            }
18236            Some(names)
18237        } else {
18238            None
18239        };
18240        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18241        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18242        // is captured on the statement so the engine can apply PG's
18243        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18244        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18245        {
18246            self.advance();
18247            let which = self.expect_ident_like()?;
18248            let ov = if which.eq_ignore_ascii_case("system") {
18249                Overriding::System
18250            } else if which.eq_ignore_ascii_case("user") {
18251                Overriding::User
18252            } else {
18253                return Err(self.err(format!(
18254                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18255                )));
18256            };
18257            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18258                return Err(self.err(format!(
18259                    "expected VALUE after OVERRIDING {}, got {:?}",
18260                    which.to_ascii_uppercase(),
18261                    self.peek()
18262                )));
18263            }
18264            self.advance();
18265            ov
18266        } else {
18267            Overriding::None
18268        };
18269        // `INSERT INTO t DEFAULT VALUES` — a single row made
18270        // entirely of column defaults. Lower to the permuted
18271        // column-list path with an empty list: every schema column
18272        // is unmapped, so the engine fills each from its default
18273        // (serials advance, plain defaults evaluate, the rest NULL).
18274        if matches!(self.peek(), Token::Default) {
18275            self.advance();
18276            if !matches!(self.peek(), Token::Values) {
18277                return Err(self.err(format!(
18278                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18279                    self.peek()
18280                )));
18281            }
18282            self.advance();
18283            if columns.is_some() {
18284                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18285            }
18286            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18287            let returning = self.parse_optional_returning()?;
18288            return Ok(Statement::Insert(InsertStatement {
18289                ctes: Vec::new(),
18290                table,
18291                alias: alias.clone(),
18292                columns: Some(Vec::new()),
18293                rows: alloc::vec![Vec::new()],
18294                select_source: None,
18295                on_conflict,
18296                returning,
18297                overriding,
18298                mysql_ignore: ignore,
18299            }));
18300        }
18301        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18302        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18303        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18304        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18305        // own WITH comes before INSERT).
18306        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18307            let select_stmt = if self.peek_is_with_kw() {
18308                self.advance();
18309                self.parse_nested_with_select()?
18310            } else {
18311                match self.parse_select_stmt()? {
18312                    Statement::Select(s) => s,
18313                    other => {
18314                        return Err(self.err(alloc::format!(
18315                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18316                        )));
18317                    }
18318                }
18319            };
18320            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18321            let returning = self.parse_optional_returning()?;
18322            return Ok(Statement::Insert(InsertStatement {
18323                ctes: Vec::new(),
18324                table,
18325                alias: alias.clone(),
18326                columns,
18327                rows: Vec::new(),
18328                select_source: Some(Box::new(select_stmt)),
18329                on_conflict,
18330                returning,
18331                overriding,
18332                mysql_ignore: ignore,
18333            }));
18334        }
18335        if !matches!(self.peek(), Token::Values) {
18336            return Err(self.err(format!(
18337                "expected VALUES or SELECT after table name, got {:?}",
18338                self.peek()
18339            )));
18340        }
18341        self.advance();
18342        if !matches!(self.peek(), Token::LParen) {
18343            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18344        }
18345        let mut rows = Vec::new();
18346        loop {
18347            // Each iteration consumes one `(expr, expr, …)` tuple.
18348            if !matches!(self.peek(), Token::LParen) {
18349                return Err(self.err(format!(
18350                    "expected '(' for next VALUES tuple, got {:?}",
18351                    self.peek()
18352                )));
18353            }
18354            self.advance();
18355            let mut tuple = Vec::new();
18356            loop {
18357                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18358                // the column's declared default for that slot. Rides out as the
18359                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18360                // path uses; the INSERT executor resolves it per target column.
18361                if matches!(self.peek(), Token::Default) {
18362                    self.advance();
18363                    tuple.push(Expr::FunctionCall {
18364                        name: "__column_default".to_string(),
18365                        args: Vec::new(),
18366                    });
18367                } else {
18368                    tuple.push(self.parse_expr(0)?);
18369                }
18370                match self.peek() {
18371                    Token::Comma => {
18372                        self.advance();
18373                    }
18374                    Token::RParen => {
18375                        self.advance();
18376                        break;
18377                    }
18378                    other => {
18379                        return Err(self.err(format!(
18380                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18381                        )));
18382                    }
18383                }
18384            }
18385            if tuple.is_empty() {
18386                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18387            }
18388            rows.push(tuple);
18389            // Continue with comma-separated tuples.
18390            if matches!(self.peek(), Token::Comma) {
18391                self.advance();
18392            } else {
18393                break;
18394            }
18395        }
18396        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18397        // to ON CONFLICT DO UPDATE with an empty conflict target
18398        // (the engine picks the table's first unique index, which
18399        // matches MySQL's any-unique-key behaviour for the common
18400        // single-key case). `VALUES(col)` in the assignments is
18401        // MySQL's spelling of EXCLUDED.col.
18402        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18403        let returning = self.parse_optional_returning()?;
18404        Ok(Statement::Insert(InsertStatement {
18405            ctes: Vec::new(),
18406            table,
18407            alias,
18408            columns,
18409            rows,
18410            select_source: None,
18411            on_conflict,
18412            returning,
18413            overriding,
18414            mysql_ignore: ignore,
18415        }))
18416    }
18417
18418    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18419    /// the incoming row's value — exactly PG's EXCLUDED.col.
18420    fn rewrite_mysql_values_refs(e: &mut Expr) {
18421        match e {
18422            Expr::FunctionCall { name, args }
18423                if name.eq_ignore_ascii_case("values")
18424                    && args.len() == 1
18425                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18426            {
18427                let Expr::Column(c) = &args[0] else {
18428                    unreachable!("guarded above");
18429                };
18430                *e = Expr::Column(crate::ast::ColumnName {
18431                    qualifier: Some("EXCLUDED".to_string()),
18432                    name: c.name.clone(),
18433                });
18434            }
18435            Expr::FunctionCall { args, .. } => {
18436                for a in args {
18437                    Self::rewrite_mysql_values_refs(a);
18438                }
18439            }
18440            Expr::Binary { lhs, rhs, .. } => {
18441                Self::rewrite_mysql_values_refs(lhs);
18442                Self::rewrite_mysql_values_refs(rhs);
18443            }
18444            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18445                Self::rewrite_mysql_values_refs(expr);
18446            }
18447            Expr::Case {
18448                operand,
18449                branches,
18450                else_branch,
18451            } => {
18452                if let Some(op) = operand {
18453                    Self::rewrite_mysql_values_refs(op);
18454                }
18455                for (w, t) in branches {
18456                    Self::rewrite_mysql_values_refs(w);
18457                    Self::rewrite_mysql_values_refs(t);
18458                }
18459                if let Some(el) = else_branch {
18460                    Self::rewrite_mysql_values_refs(el);
18461                }
18462            }
18463            _ => {}
18464        }
18465    }
18466
18467    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18468    /// clause sitting between the INSERT body and the trailing
18469    /// RETURNING. All keywords come in as bare idents; `ON` is
18470    /// a reserved Token though.
18471    fn parse_optional_on_conflict(
18472        &mut self,
18473    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18474        if !matches!(self.peek(), Token::On) {
18475            return Ok(None);
18476        }
18477        // Peek further: we want exactly "ON CONFLICT ...". If the
18478        // next ident isn't "conflict", let some other parser handle.
18479        let next_is_conflict = matches!(
18480            self.tokens.get(self.pos + 1),
18481            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18482        );
18483        if !next_is_conflict {
18484            return Ok(None);
18485        }
18486        self.advance(); // ON
18487        self.advance(); // CONFLICT
18488        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18489        // the constraint instead of listing columns (the pg_dump
18490        // form); the engine resolves it.
18491        let mut constraint_name: Option<String> = None;
18492        if matches!(self.peek(), Token::On) {
18493            self.advance(); // ON
18494            match self.advance() {
18495                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18496                }
18497                other => {
18498                    return Err(self.err(alloc::format!(
18499                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18500                    )));
18501                }
18502            }
18503            constraint_name = Some(self.expect_ident_like()?);
18504        }
18505        // Optional `(col [, col]*)` target list.
18506        let mut target_columns: Vec<String> = Vec::new();
18507        if matches!(self.peek(), Token::LParen) {
18508            self.advance();
18509            loop {
18510                target_columns.push(self.expect_ident_like()?);
18511                match self.peek() {
18512                    Token::Comma => {
18513                        self.advance();
18514                    }
18515                    Token::RParen => {
18516                        self.advance();
18517                        break;
18518                    }
18519                    other => {
18520                        return Err(self.err(alloc::format!(
18521                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18522                        )));
18523                    }
18524                }
18525            }
18526        }
18527        // v7.39 (round 240) — optional index predicate after the target
18528        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18529        // PARTIAL unique index; SPG's arbiters are full indexes, which
18530        // satisfy any predicate, so it is parsed and carried but not
18531        // consulted (recorded residual: partial-unique-index arbiters).
18532        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18533            self.advance();
18534            Some(self.parse_expr(0)?)
18535        } else {
18536            None
18537        };
18538        // Required `DO`.
18539        match self.advance() {
18540            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18541            other => {
18542                return Err(self.err(alloc::format!(
18543                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18544                )));
18545            }
18546        }
18547        // Action: NOTHING | UPDATE SET …
18548        let action = match self.advance() {
18549            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18550                crate::ast::OnConflictAction::Nothing
18551            }
18552            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18553                self.parse_on_conflict_update_action()?
18554            }
18555            other => {
18556                return Err(self.err(alloc::format!(
18557                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18558                )));
18559            }
18560        };
18561        Ok(Some(crate::ast::OnConflictClause {
18562            target_columns,
18563            index_where,
18564            constraint_name,
18565            mysql_lowered: false,
18566            action,
18567        }))
18568    }
18569
18570    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18571    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18572    /// consumed `UPDATE`.
18573    fn parse_on_conflict_update_action(
18574        &mut self,
18575    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18576        // `SET`
18577        match self.advance() {
18578            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18579            other => {
18580                return Err(self.err(alloc::format!(
18581                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18582                )));
18583            }
18584        }
18585        let mut assignments: Vec<(String, Expr)> = Vec::new();
18586        loop {
18587            let col = self.expect_ident_like()?;
18588            if !matches!(self.peek(), Token::Eq) {
18589                return Err(self.err(alloc::format!(
18590                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18591                    self.peek()
18592                )));
18593            }
18594            self.advance();
18595            let value = self.parse_expr(0)?;
18596            assignments.push((col, value));
18597            if matches!(self.peek(), Token::Comma) {
18598                self.advance();
18599                continue;
18600            }
18601            break;
18602        }
18603        let where_ = if matches!(self.peek(), Token::Where) {
18604            self.advance();
18605            Some(self.parse_expr(0)?)
18606        } else {
18607            None
18608        };
18609        Ok(crate::ast::OnConflictAction::Update {
18610            assignments,
18611            where_,
18612        })
18613    }
18614
18615    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18616        let mut items = Vec::new();
18617        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18618        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18619        // answers one zero-column row per row of t, and a bare `SELECT`
18620        // answers a single zero-column row. SPG required at least one
18621        // item, so both were syntax errors. Recognised by the token that
18622        // follows — nothing that can start an expression appears here.
18623        if self.select_list_is_empty_here() {
18624            return Ok(items);
18625        }
18626        loop {
18627            items.push(self.parse_select_item()?);
18628            if matches!(self.peek(), Token::Comma) {
18629                self.advance();
18630            } else {
18631                break;
18632            }
18633        }
18634        Ok(items)
18635    }
18636
18637    /// Is the target list empty at this point — i.e. does the next token
18638    /// end the SELECT's item list rather than start an item?
18639    fn select_list_is_empty_here(&self) -> bool {
18640        match self.peek() {
18641            Token::From
18642            | Token::Where
18643            | Token::Group
18644            | Token::Having
18645            | Token::Order
18646            | Token::Limit
18647            | Token::Offset
18648            | Token::Semicolon
18649            | Token::RParen
18650            | Token::Union
18651            | Token::Except
18652            | Token::Eof => true,
18653            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18654            // with unreserved keywords, so they arrive as plain idents.
18655            Token::Ident(s) => {
18656                s.eq_ignore_ascii_case("fetch")
18657                    || s.eq_ignore_ascii_case("window")
18658                    || s.eq_ignore_ascii_case("intersect")
18659            }
18660            _ => false,
18661        }
18662    }
18663
18664    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18665        if matches!(self.peek(), Token::Star) {
18666            self.advance();
18667            return Ok(SelectItem::Wildcard);
18668        }
18669        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18670        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18671        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18672        // `<ident> . *` with nothing binding tighter.
18673        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18674            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18675                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18676            {
18677                self.advance(); // qualifier
18678                self.advance(); // .
18679                self.advance(); // *
18680                return Ok(SelectItem::QualifiedWildcard(q));
18681            }
18682        }
18683        let start_tok = self.pos;
18684        let expr = self.parse_expr(0)?;
18685        let end_tok = self.consumed_pos();
18686        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18687        // multi-column function returns into columns. Marked here and lowered in
18688        // `parse_bare_select`, where the FROM clause is in hand.
18689        if matches!(self.peek(), Token::Dot)
18690            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18691        {
18692            self.advance(); // .
18693            self.advance(); // *
18694            return Ok(SelectItem::Expr {
18695                expr: Expr::FunctionCall {
18696                    name: "__record_expand".to_string(),
18697                    args: alloc::vec![expr],
18698                },
18699                alias: None,
18700            });
18701        }
18702        let alias = match self.parse_optional_alias()? {
18703            Some(a) => Some(a),
18704            None => self.mysql_item_label(&expr, start_tok, end_tok),
18705        };
18706        Ok(SelectItem::Expr { expr, alias })
18707    }
18708
18709    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18710    /// carries no `AS`, filled in here so every downstream path reports it
18711    /// without knowing the rule. `None` leaves the item un-aliased, which is
18712    /// what a PG session always gets.
18713    ///
18714    /// Measured against MariaDB 11, three rules and no more:
18715    ///
18716    /// | item             | label      | why                          |
18717    /// |------------------|------------|------------------------------|
18718    /// | `lbl.a`          | `a`        | a column reports its name    |
18719    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18720    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18721    ///
18722    /// The third is why this lives in the parser at all: the label is the
18723    /// text the client WROTE, down to the spacing, so it cannot be printed
18724    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18725    ///
18726    /// Comments survive, and that is right: through a `mariadb` CLI both
18727    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18728    /// CLIENT stripping the comment before it sends. Asked over the raw
18729    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18730    /// produces.
18731    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18732        if !self.mysql_dialect {
18733            return None;
18734        }
18735        match expr {
18736            // A column already reports its own name downstream; naming it
18737            // again here would only re-state the qualifier the label drops.
18738            Expr::Column(_) => None,
18739            Expr::Literal(Literal::String(v)) => Some(v.clone()),
18740            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18741        }
18742    }
18743
18744    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18745    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18746    /// with PG's default column1..columnN names; subsequent rows
18747    /// chain as UNION ALL peers. Shared by the FROM-position
18748    /// `( VALUES … )` arm and the top-level bare VALUES statement.
18749    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18750        let mut row_selects: Vec<SelectStatement> = Vec::new();
18751        loop {
18752            if !matches!(self.peek(), Token::LParen) {
18753                return Err(self.err(alloc::format!(
18754                    "expected '(' to start a VALUES row, got {:?}",
18755                    self.peek()
18756                )));
18757            }
18758            self.advance(); // (
18759            let mut items: Vec<SelectItem> = Vec::new();
18760            loop {
18761                let expr = self.parse_expr(0)?;
18762                items.push(SelectItem::Expr {
18763                    expr,
18764                    alias: Some(alloc::format!("column{}", items.len() + 1)),
18765                });
18766                match self.peek() {
18767                    Token::Comma => {
18768                        self.advance();
18769                    }
18770                    Token::RParen => break,
18771                    other => {
18772                        return Err(self.err(alloc::format!(
18773                            "expected ',' or ')' in VALUES row, got {other:?}"
18774                        )));
18775                    }
18776                }
18777            }
18778            self.advance(); // )
18779            row_selects.push(SelectStatement {
18780                locking: None,
18781                ctes: Vec::new(),
18782                distinct: false,
18783                distinct_on: Vec::new(),
18784                items,
18785                from: None,
18786                where_: None,
18787                group_by: None,
18788                group_by_all: false,
18789                having: None,
18790                unions: Vec::new(),
18791                order_by: Vec::new(),
18792                limit: None,
18793                offset: None,
18794                limit_with_ties: false,
18795                window_check_exprs: Vec::new(),
18796            });
18797            if matches!(self.peek(), Token::Comma) {
18798                self.advance();
18799                continue;
18800            }
18801            break;
18802        }
18803        let mut head = row_selects.remove(0);
18804        head.unions = row_selects
18805            .into_iter()
18806            .map(|s| (UnionKind::All, s))
18807            .collect();
18808        Ok(head)
18809    }
18810
18811    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18812        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18813        // children. It was read as a table NAMED `only`, so the query
18814        // failed on `relation "only" does not exist`.
18815        //
18816        // v7.39 (round 644) — and it is no longer a no-op. Round 621
18817        // absorbed the keyword, reasoning that SPG's children are
18818        // separate relations a plain scan does not descend into, so ONLY
18819        // already described the scan. That stopped being true when a
18820        // partition parent started unioning its children: measured,
18821        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18822        // where PG answers 0. The flag is carried now.
18823        let mut only = false;
18824        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18825            && matches!(
18826                self.tokens.get(self.pos + 1),
18827                Some(Token::Ident(_) | Token::QuotedIdent(_))
18828            )
18829        {
18830            only = true;
18831            self.advance();
18832        }
18833        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18834        // for these SRFs the keyword is noise at parse time: the
18835        // join executor already substitutes outer-column references
18836        // into unnest_expr / generate_series_args per outer row
18837        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18838        // licences the correlation even without the keyword. Absorb
18839        // it and fall through to the SRF arms below.
18840        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18841        // just the four builtin SRFs: a user set-returning function on a JOIN's
18842        // right side is the whole point of LATERAL. The keyword stays noise at
18843        // parse time — the join executor substitutes the outer row into the
18844        // call's arguments per outer row.
18845        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18846            && matches!(
18847                self.tokens.get(self.pos + 1),
18848                // The json_each family has its OWN `LATERAL …` arm below, which
18849                // needs to see the keyword — absorbing it here would send those
18850                // calls down the generic table-function channel instead.
18851                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18852            )
18853            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18854        {
18855            self.advance(); // LATERAL
18856        }
18857        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18858        // set-returning function whose argument may reference a
18859        // preceding FROM item. We rewrite this to
18860        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18861        // AS __srf__) AS <alias>` so the existing LATERAL subquery
18862        // executor handles per-outer-row evaluation and the
18863        // SRF-primary jsonb_each_text path handles the inner
18864        // materialisation. Sentori 0067 backfill is the dogfood
18865        // shape.
18866        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18867            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18868            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18869        {
18870            self.advance(); // LATERAL
18871            let each_fn = match self.peek() {
18872                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18873                _ => unreachable!(),
18874            };
18875            self.advance(); // jsonb_each[_text] / json_each[_text]
18876            self.advance(); // (
18877            let arg = self.parse_expr(0)?;
18878            if !matches!(self.peek(), Token::RParen) {
18879                return Err(self.err(alloc::format!(
18880                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18881                    self.peek()
18882                )));
18883            }
18884            self.advance();
18885            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18886            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18887            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18888            //               FROM jsonb_each_text(<arg>) AS __srf__
18889            // PG's `AS kv(key, value)` column-alias list maps
18890            // positions to names; default to (key, value) when
18891            // omitted (matching the SRF's natural column names).
18892            let srf_alias = "__srf__".to_string();
18893            let key_alias = column_aliases
18894                .first()
18895                .cloned()
18896                .unwrap_or_else(|| "key".to_string());
18897            let value_alias = column_aliases
18898                .get(1)
18899                .cloned()
18900                .unwrap_or_else(|| "value".to_string());
18901            let inner_select = crate::ast::SelectStatement {
18902                locking: None,
18903                ctes: Vec::new(),
18904                distinct: false,
18905                distinct_on: Vec::new(),
18906                items: alloc::vec![
18907                    crate::ast::SelectItem::Expr {
18908                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18909                            qualifier: Some(srf_alias.clone()),
18910                            name: "key".to_string(),
18911                        }),
18912                        alias: Some(key_alias),
18913                    },
18914                    crate::ast::SelectItem::Expr {
18915                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18916                            qualifier: Some(srf_alias.clone()),
18917                            name: "value".to_string(),
18918                        }),
18919                        alias: Some(value_alias),
18920                    },
18921                ],
18922                from: Some(crate::ast::FromClause {
18923                    primary: TableRef {
18924                        name: srf_alias.clone(),
18925                        alias: Some(srf_alias.clone()),
18926                        only: false,
18927                        as_of_segment: None,
18928                        unnest_expr: None,
18929                        unnest_column_aliases: Vec::new(),
18930                        with_ordinality: false,
18931                        generate_series_args: None,
18932                        lateral_subquery: None,
18933                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18934                        table_fn_call: None,
18935                        rows_from: None,
18936                        json_table: None,
18937                        scalar_fn_item: false,
18938                    },
18939                    joins: Vec::new(),
18940                }),
18941                where_: None,
18942                group_by: None,
18943                group_by_all: false,
18944                having: None,
18945                unions: Vec::new(),
18946                order_by: Vec::new(),
18947                limit: None,
18948                offset: None,
18949                limit_with_ties: false,
18950                window_check_exprs: Vec::new(),
18951            };
18952            return Ok(TableRef {
18953                name: alias.clone(),
18954                alias: Some(alias),
18955                only: false,
18956                as_of_segment: None,
18957                unnest_expr: None,
18958                unnest_column_aliases: Vec::new(),
18959                with_ordinality: false,
18960                generate_series_args: None,
18961                lateral_subquery: Some(Box::new(inner_select)),
18962                jsonb_each_text_arg: None,
18963                table_fn_call: None,
18964                rows_from: None,
18965                json_table: None,
18966                scalar_fn_item: false,
18967            });
18968        }
18969        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18970        // without an explicit `LATERAL` keyword is the same shape
18971        // PG accepts (SRF naturally licences lateral correlation).
18972        // We mirror the LATERAL rewrite when the argument syntactic-
18973        // ally references an outer column (Column { qualifier:
18974        // Some(_), … }). For simplicity we apply the rewrite
18975        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18976        // in the FROM-list — caller-side join parsing positions
18977        // this peek correctly.
18978        // (Implementation note: detection lives below; the LATERAL
18979        // branch above already covers the explicit form; the bare
18980        // form falls through to the plain SRF arm and the engine
18981        // treats it as a constant-arg SRF if no outer reference is
18982        // present.)
18983        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18984        // table. Detect at the head so it claims precedence over
18985        // every other table-ref shape (unnest / generate_series /
18986        // bare ident); the lateral subquery itself follows the
18987        // regular SELECT grammar.
18988        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18989        // t(cols)`. Each row lowers to a constant SELECT with PG's
18990        // default column1..columnN names; subsequent rows chain as
18991        // UNION ALL peers. The result rides the derived-table
18992        // lateral_subquery channel — zero executor work.
18993        if matches!(self.peek(), Token::LParen)
18994            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18995        {
18996            self.advance(); // (
18997            self.advance(); // VALUES
18998            let head = self.parse_values_rows_body()?;
18999            if !matches!(self.peek(), Token::RParen) {
19000                return Err(self.err(alloc::format!(
19001                    "expected ')' after VALUES list, got {:?}",
19002                    self.peek()
19003                )));
19004            }
19005            self.advance();
19006            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19007            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19008            return Ok(TableRef {
19009                name,
19010                alias: alias_ident,
19011                only: false,
19012                as_of_segment: None,
19013                unnest_expr: None,
19014                unnest_column_aliases: column_aliases,
19015                with_ordinality: false,
19016                generate_series_args: None,
19017                lateral_subquery: Some(Box::new(head)),
19018                jsonb_each_text_arg: None,
19019                table_fn_call: None,
19020                rows_from: None,
19021                json_table: None,
19022                scalar_fn_item: false,
19023            });
19024        }
19025        // v7.37.17 (17.6 siblings) — plain derived table:
19026        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19027        // lateral_subquery channel the explicit LATERAL form uses —
19028        // an uncorrelated inner SELECT executes identically. The
19029        // inner parse carries UNION tails (they live on
19030        // SelectStatement.unions).
19031        // v7.37 D.20 — the derived-table inner may itself be a
19032        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19033        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19034        // bare `(SELECT …)`. parse_one_statement already routes a leading
19035        // `(` set-op group (its LParen arm) and a leading WITH
19036        // (parse_with_cte_then_select), so widen the second-token gate to
19037        // Select | LParen | WITH.
19038        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19039        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19040        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19041        // has existed since the shorthand landed and `parse_bare_select`
19042        // already routes it ("valid anywhere a SELECT head is"); what was
19043        // missing is this second-token gate, and the CTE body's dispatch
19044        // below. Round 868 found both by putting the shorthand in a
19045        // subquery — the top-level forms had been the only ones tested.
19046        if matches!(self.peek(), Token::LParen)
19047            && (matches!(
19048                self.tokens.get(self.pos + 1),
19049                Some(Token::Select | Token::LParen | Token::Table)
19050            ) || matches!(self.tokens.get(self.pos + 1),
19051                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19052        {
19053            self.advance(); // (
19054            let inner = match self.parse_one_statement()? {
19055                Statement::Select(s) => s,
19056                other => {
19057                    return Err(self.err(alloc::format!(
19058                        "expected SELECT inside derived table ( … ), got {other:?}"
19059                    )));
19060                }
19061            };
19062            if !matches!(self.peek(), Token::RParen) {
19063                return Err(self.err(alloc::format!(
19064                    "expected ')' after derived-table subquery, got {:?}",
19065                    self.peek()
19066                )));
19067            }
19068            self.advance();
19069            // `AS t(a, b)` column-alias list rides the
19070            // unnest_column_aliases field (same positional-rename
19071            // contract the unnest SRFs use).
19072            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19073            let name = alias_ident
19074                .clone()
19075                .unwrap_or_else(|| "subquery".to_string());
19076            return Ok(TableRef {
19077                name,
19078                alias: alias_ident,
19079                only: false,
19080                as_of_segment: None,
19081                unnest_expr: None,
19082                unnest_column_aliases: column_aliases,
19083                with_ordinality: false,
19084                generate_series_args: None,
19085                lateral_subquery: Some(Box::new(inner)),
19086                jsonb_each_text_arg: None,
19087                table_fn_call: None,
19088                rows_from: None,
19089                json_table: None,
19090                scalar_fn_item: false,
19091            });
19092        }
19093        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19094            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19095        {
19096            self.advance(); // LATERAL
19097            self.advance(); // (
19098            // Parse the inner SELECT.
19099            let inner = match self.parse_one_statement()? {
19100                Statement::Select(s) => s,
19101                other => {
19102                    return Err(self.err(alloc::format!(
19103                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19104                    )));
19105                }
19106            };
19107            if !matches!(self.peek(), Token::RParen) {
19108                return Err(self.err(alloc::format!(
19109                    "expected ')' after LATERAL subquery, got {:?}",
19110                    self.peek()
19111                )));
19112            }
19113            self.advance();
19114            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19115            // `(VALUES …) t(g)` derived table round-trips through view-body
19116            // Display, which renders on the lateral_subquery channel).
19117            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19118            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19119            return Ok(TableRef {
19120                name,
19121                alias: alias_ident,
19122                only: false,
19123                as_of_segment: None,
19124                unnest_expr: None,
19125                unnest_column_aliases: column_aliases,
19126                with_ordinality: false,
19127                generate_series_args: None,
19128                lateral_subquery: Some(Box::new(inner)),
19129                jsonb_each_text_arg: None,
19130                table_fn_call: None,
19131                rows_from: None,
19132                json_table: None,
19133                scalar_fn_item: false,
19134            });
19135        }
19136        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19137        // function as a FROM item. Emits one row per (key, value)
19138        // pair in the JSONB object argument as TEXT columns. May
19139        // be wrapped in CROSS JOIN LATERAL when the argument
19140        // references a preceding FROM item (sentori migration
19141        // 0067 backfill shape: `CROSS JOIN LATERAL
19142        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19143        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19144            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19145        {
19146            let each_fn = match self.peek() {
19147                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19148                _ => unreachable!(),
19149            };
19150            self.advance(); // jsonb_each[_text] / json_each[_text]
19151            self.advance(); // (
19152            let arg = self.parse_expr(0)?;
19153            if !matches!(self.peek(), Token::RParen) {
19154                return Err(self.err(alloc::format!(
19155                    "expected ')' after {each_fn}() argument, got {:?}",
19156                    self.peek()
19157                )));
19158            }
19159            self.advance();
19160            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19161            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19162            return Ok(TableRef {
19163                name,
19164                alias: alias_ident,
19165                only: false,
19166                as_of_segment: None,
19167                unnest_expr: None,
19168                // `AS t(k, v)` renames key/value positionally, same as the
19169                // LATERAL-position form already does.
19170                unnest_column_aliases: column_aliases,
19171                with_ordinality: false,
19172                generate_series_args: None,
19173                lateral_subquery: None,
19174                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19175                table_fn_call: None,
19176                rows_from: None,
19177                json_table: None,
19178                scalar_fn_item: false,
19179            });
19180        }
19181        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19182        // (+ json_ variants) — record-returning JSON functions with a
19183        // column-definition list. Desugar to a derived table that
19184        // projects each declared column from the JSON via `->>` + a cast,
19185        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19186        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19187            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19188        {
19189            return self.parse_json_to_record_from();
19190        }
19191        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19192        // row is a text[] of capture groups, so it cannot desugar to unnest
19193        // (that would flatten the array). Wrap it as a derived table
19194        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19195        // SRF path already emits one text[] row per match. PG names the column
19196        // `regexp_matches`; an `AS a(col)` alias overrides it.
19197        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19198                if s.eq_ignore_ascii_case("regexp_matches"))
19199            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19200        {
19201            self.advance(); // fn name
19202            self.advance(); // (
19203            let mut fn_args: Vec<Expr> = Vec::new();
19204            loop {
19205                fn_args.push(self.parse_expr(0)?);
19206                if matches!(self.peek(), Token::Comma) {
19207                    self.advance();
19208                    continue;
19209                }
19210                break;
19211            }
19212            if !matches!(self.peek(), Token::RParen) {
19213                return Err(self.err(alloc::format!(
19214                    "expected ')' after regexp_matches() arguments, got {:?}",
19215                    self.peek()
19216                )));
19217            }
19218            self.advance();
19219            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19220            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19221            // it, so it died on the `with` token while every other table function
19222            // accepted it.
19223            let with_ordinality = self.absorb_with_ordinality();
19224            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19225            let table_alias = alias_ident
19226                .clone()
19227                .unwrap_or_else(|| "regexp_matches".to_string());
19228            // PG names a single-column function's output column after the ALIAS
19229            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19230            // `m` reads as that column and not as a whole-row composite. Naming
19231            // it after the function regardless made `SELECT m[1] FROM … AS m`
19232            // subscript a record.
19233            let col_name = column_aliases
19234                .first()
19235                .cloned()
19236                .or_else(|| alias_ident.clone())
19237                .unwrap_or_else(|| "regexp_matches".to_string());
19238            let inner = crate::ast::SelectStatement {
19239                locking: None,
19240                ctes: Vec::new(),
19241                distinct: false,
19242                distinct_on: Vec::new(),
19243                items: alloc::vec![SelectItem::Expr {
19244                    expr: Expr::FunctionCall {
19245                        name: "regexp_matches".to_string(),
19246                        args: fn_args,
19247                    },
19248                    alias: Some(col_name),
19249                }],
19250                from: None,
19251                where_: None,
19252                group_by: None,
19253                group_by_all: false,
19254                having: None,
19255                unions: Vec::new(),
19256                order_by: Vec::new(),
19257                limit: None,
19258                offset: None,
19259                limit_with_ties: false,
19260                window_check_exprs: Vec::new(),
19261            };
19262            return Ok(TableRef {
19263                name: table_alias.clone(),
19264                alias: Some(table_alias),
19265                only: false,
19266                as_of_segment: None,
19267                unnest_expr: None,
19268                unnest_column_aliases: column_aliases,
19269                with_ordinality,
19270                generate_series_args: None,
19271                lateral_subquery: Some(Box::new(inner)),
19272                jsonb_each_text_arg: None,
19273                table_fn_call: None,
19274                rows_from: None,
19275                json_table: None,
19276                // regexp_matches returns text[], a base type: `SELECT m FROM
19277                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19278                scalar_fn_item: true,
19279            });
19280        }
19281        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19282        // / json_ variants as a FROM item. Rewritten into
19283        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19284        // elements as a TEXT array, and the existing unnest SRF path
19285        // materialises one row per element. PG's natural column name
19286        // is `value`; an `AS a(col)` column-alias list overrides it.
19287        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19288                if s.eq_ignore_ascii_case("jsonb_array_elements")
19289                    || s.eq_ignore_ascii_case("json_array_elements")
19290                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19291                    || s.eq_ignore_ascii_case("json_array_elements_text")
19292                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19293                    || s.eq_ignore_ascii_case("json_object_keys")
19294                    || s.eq_ignore_ascii_case("jsonb_path_query")
19295                    || s.eq_ignore_ascii_case("json_path_query")
19296                    || s.eq_ignore_ascii_case("generate_subscripts")
19297                    || s.eq_ignore_ascii_case("string_to_table")
19298                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19299            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19300        {
19301            let fn_name = match self.peek() {
19302                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19303                _ => unreachable!(),
19304            };
19305            self.advance(); // fn name
19306            self.advance(); // (
19307            let mut fn_args: Vec<Expr> = Vec::new();
19308            loop {
19309                fn_args.push(self.parse_expr(0)?);
19310                if matches!(self.peek(), Token::Comma) {
19311                    self.advance();
19312                    continue;
19313                }
19314                break;
19315            }
19316            if !matches!(self.peek(), Token::RParen) {
19317                return Err(self.err(alloc::format!(
19318                    "expected ')' after {fn_name}() arguments, got {:?}",
19319                    self.peek()
19320                )));
19321            }
19322            self.advance();
19323            let with_ordinality = self.absorb_with_ordinality();
19324            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19325            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19326            // PG's natural column name: the array-elements SRFs
19327            // declare an OUT parameter `value`; jsonb_object_keys
19328            // and generate_subscripts have none, so the column is
19329            // named after the function. A bare table alias on a
19330            // single-column SRF renames the column too (PG: `FROM
19331            // generate_subscripts(a, 1) AS s` projects column s) —
19332            // except for the OUT-parameter SRFs, whose column stays
19333            // `value` under a bare alias.
19334            let natural_col = if fn_name.ends_with("_array_elements")
19335                || fn_name.ends_with("_array_elements_text")
19336            {
19337                "value".to_string()
19338            } else {
19339                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19340            };
19341            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19342            // Keep any further entries — the second names the
19343            // ordinality column under WITH ORDINALITY.
19344            srf_cols.extend(column_aliases.into_iter().skip(1));
19345            // The *_to_table SRFs are row-streams over the existing
19346            // *_to_array scalars — map the call target; the display
19347            // name (alias / column defaults) keeps the SRF spelling.
19348            let call_name = match fn_name.as_str() {
19349                "string_to_table" => "string_to_array".to_string(),
19350                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19351                _ => fn_name,
19352            };
19353            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19354            // preceding FROM item (bare or qualified column) is correlated;
19355            // route it through the per-outer-row lateral channel.
19356            let expr = crate::ast::Expr::FunctionCall {
19357                name: call_name,
19358                args: fn_args,
19359            };
19360            let correlated = Self::expr_has_any_column(&expr);
19361            let tref = TableRef {
19362                name,
19363                alias: alias_ident,
19364                only: false,
19365                as_of_segment: None,
19366                unnest_expr: Some(Box::new(expr)),
19367                unnest_column_aliases: srf_cols,
19368                with_ordinality,
19369                generate_series_args: None,
19370                lateral_subquery: None,
19371                jsonb_each_text_arg: None,
19372                table_fn_call: None,
19373                rows_from: None,
19374                json_table: None,
19375                // Each of these returns a BASE type (jsonb / text / int), so the item's
19376                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19377                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19378                scalar_fn_item: !with_ordinality,
19379            };
19380            return Ok(if correlated {
19381                Self::wrap_correlated_srf(tref)
19382            } else {
19383                tref
19384            });
19385        }
19386        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19387        // explicit parallel-zip syntax. Each entry lowers to its
19388        // array-returning scalar form (unnest(x) → x itself; the
19389        // FROM-SRF rewrite family → their scalar array calls) and
19390        // the list rides the multi-arg unnest zip channel:
19391        // NULL-padded to the longest, WITH ORDINALITY appends the
19392        // counter. generate_series has no scalar array form and
19393        // errors honestly.
19394        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19395            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19396            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19397        {
19398            self.advance(); // ROWS
19399            self.advance(); // FROM
19400            self.advance(); // (
19401            let mut entries: Vec<Expr> = Vec::new();
19402            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19403            // Used only when some entry has no array form.
19404            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19405            loop {
19406                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19407                if !matches!(self.peek(), Token::LParen) {
19408                    return Err(self.err(alloc::format!(
19409                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19410                        self.peek()
19411                    )));
19412                }
19413                self.advance();
19414                let mut fn_args: Vec<Expr> = Vec::new();
19415                if !matches!(self.peek(), Token::RParen) {
19416                    loop {
19417                        fn_args.push(self.parse_expr(0)?);
19418                        if matches!(self.peek(), Token::Comma) {
19419                            self.advance();
19420                            continue;
19421                        }
19422                        break;
19423                    }
19424                }
19425                if !matches!(self.peek(), Token::RParen) {
19426                    return Err(self.err(alloc::format!(
19427                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19428                        self.peek()
19429                    )));
19430                }
19431                self.advance();
19432                let entry = match fn_name.as_str() {
19433                    "unnest" => {
19434                        if fn_args.len() != 1 {
19435                            return Err(
19436                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19437                            );
19438                        }
19439                        fn_args.pop().expect("len checked")
19440                    }
19441                    "jsonb_array_elements"
19442                    | "json_array_elements"
19443                    | "jsonb_array_elements_text"
19444                    | "json_array_elements_text"
19445                    | "jsonb_object_keys"
19446                    | "json_object_keys"
19447                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19448                        name: fn_name,
19449                        args: fn_args,
19450                    },
19451                    "string_to_table" => crate::ast::Expr::FunctionCall {
19452                        name: "string_to_array".to_string(),
19453                        args: fn_args,
19454                    },
19455                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19456                        name: "regexp_split_to_array".to_string(),
19457                        args: fn_args,
19458                    },
19459                    // v7.39 (read01 round 74) — an SRF with no array form
19460                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19461                    // scalar expression to zip, so the WHOLE list switches to the
19462                    // rows_from channel, which runs each function and zips the
19463                    // rows themselves. The all-array case keeps the old lowering:
19464                    // it is well-trodden and this must not disturb it.
19465                    _ => {
19466                        generic.push((fn_name, fn_args));
19467                        if matches!(self.peek(), Token::Comma) {
19468                            self.advance();
19469                            continue;
19470                        }
19471                        break;
19472                    }
19473                };
19474                generic.push((
19475                    // The array-able entries carry their lowered expr along, so a
19476                    // MIXED list still works: the engine sees the scalar array
19477                    // form and unnests it.
19478                    "__array".to_string(),
19479                    alloc::vec![entry.clone()],
19480                ));
19481                entries.push(entry);
19482                if matches!(self.peek(), Token::Comma) {
19483                    self.advance();
19484                    continue;
19485                }
19486                break;
19487            }
19488            if !matches!(self.peek(), Token::RParen) {
19489                return Err(self.err(alloc::format!(
19490                    "expected ')' to close ROWS FROM, got {:?}",
19491                    self.peek()
19492                )));
19493            }
19494            self.advance();
19495            let with_ordinality = self.absorb_with_ordinality();
19496            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19497            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19498            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19499            // list rides the generic channel.
19500            if generic.iter().any(|(n, _)| n != "__array") {
19501                let correlated = generic
19502                    .iter()
19503                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19504                let tref = TableRef {
19505                    name,
19506                    alias: alias_ident,
19507                    only: false,
19508                    as_of_segment: None,
19509                    unnest_expr: None,
19510                    unnest_column_aliases,
19511                    with_ordinality,
19512                    generate_series_args: None,
19513                    lateral_subquery: None,
19514                    jsonb_each_text_arg: None,
19515                    table_fn_call: None,
19516                    rows_from: Some(generic),
19517                    json_table: None,
19518                    scalar_fn_item: false,
19519                };
19520                return Ok(if correlated {
19521                    Self::wrap_correlated_srf(tref)
19522                } else {
19523                    tref
19524                });
19525            }
19526            let correlated = entries.iter().any(Self::expr_has_any_column);
19527            let expr = if entries.len() == 1 {
19528                entries.pop().expect("len checked")
19529            } else {
19530                crate::ast::Expr::FunctionCall {
19531                    name: "__unnest_zip".to_string(),
19532                    args: entries,
19533                }
19534            };
19535            let tref = TableRef {
19536                name,
19537                alias: alias_ident,
19538                only: false,
19539                as_of_segment: None,
19540                unnest_expr: Some(Box::new(expr)),
19541                unnest_column_aliases,
19542                with_ordinality,
19543                generate_series_args: None,
19544                lateral_subquery: None,
19545                jsonb_each_text_arg: None,
19546                table_fn_call: None,
19547                rows_from: None,
19548                json_table: None,
19549                scalar_fn_item: false,
19550            };
19551            return Ok(if correlated {
19552                Self::wrap_correlated_srf(tref)
19553            } else {
19554                tref
19555            });
19556        }
19557        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19558        // source. Detect at the head before the bare-ident fallback;
19559        // unnest is not a reserved token.
19560        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19561            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19562        {
19563            self.advance(); // unnest
19564            self.advance(); // (
19565            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19566            while matches!(self.peek(), Token::Comma) {
19567                self.advance();
19568                srf_args.push(self.parse_expr(0)?);
19569            }
19570            if !matches!(self.peek(), Token::RParen) {
19571                return Err(self.err(alloc::format!(
19572                    "expected ')' after unnest() argument, got {:?}",
19573                    self.peek()
19574                )));
19575            }
19576            self.advance();
19577            // Multi-arg unnest(a, b, …) zips the arrays in
19578            // parallel, NULL-padding to the longest (PG's ROWS
19579            // FROM shorthand). Lower onto the unnest channel as an
19580            // internal marker call the executors unpack.
19581            let expr = if srf_args.len() == 1 {
19582                srf_args.pop().expect("len checked")
19583            } else {
19584                crate::ast::Expr::FunctionCall {
19585                    name: "__unnest_zip".to_string(),
19586                    args: srf_args,
19587                }
19588            };
19589            let with_ordinality = self.absorb_with_ordinality();
19590            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19591            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19592            let correlated = Self::expr_has_any_column(&expr);
19593            let tref = TableRef {
19594                name,
19595                alias: alias_ident,
19596                only: false,
19597                as_of_segment: None,
19598                unnest_expr: Some(Box::new(expr)),
19599                unnest_column_aliases,
19600                with_ordinality,
19601                generate_series_args: None,
19602                lateral_subquery: None,
19603                jsonb_each_text_arg: None,
19604                table_fn_call: None,
19605                rows_from: None,
19606                json_table: None,
19607                scalar_fn_item: false,
19608            };
19609            return Ok(if correlated {
19610                Self::wrap_correlated_srf(tref)
19611            } else {
19612                tref
19613            });
19614        }
19615        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19616        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19617        // generic table-fn arg parser can't read), so it is intercepted
19618        // here BEFORE the generic dispatch. The doc expr may reference
19619        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19620        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19621                if s.eq_ignore_ascii_case("json_table"))
19622            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19623        {
19624            let tref = self.parse_json_table_ref()?;
19625            let correlated = tref
19626                .json_table
19627                .as_deref()
19628                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19629            return Ok(if correlated {
19630                Self::wrap_correlated_srf(tref)
19631            } else {
19632                tref
19633            });
19634        }
19635        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19636        // functions dispatched by name (`pg_partition_tree('t')`,
19637        // `pg_partition_ancestors('t')`). Same head-detection shape as
19638        // unnest; the engine executor owns the row shape per function.
19639        // v7.39 (read01 round 65) — and a USER function in FROM position
19640        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19641        // (generate_series / unnest / the json_each family) keep it — their arms
19642        // sit further down, so they are excluded here by name rather than by
19643        // ordering. Anything else that is an ident followed by `(` is a table
19644        // function; the engine executor decides whether it is a builtin, a
19645        // set-returning user function, or an error.
19646        // 7.38.1 S5.1 — pg_dump spells its table functions
19647        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19648        // strip the pg_catalog prefix here so the same head-detection
19649        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19650        // meaning.
19651        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19652            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19653            && matches!(
19654                self.tokens.get(self.pos + 2),
19655                Some(Token::Ident(_) | Token::QuotedIdent(_))
19656            )
19657            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19658        {
19659            self.advance(); // pg_catalog
19660            self.advance(); // .
19661        }
19662        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19663                if !s.eq_ignore_ascii_case("generate_series")
19664                    && !s.eq_ignore_ascii_case("unnest")
19665                    && !is_json_each_name(s))
19666            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19667        {
19668            // Body out-of-line — this parse sits on the FROM/subquery
19669            // recursion chain (debug frame-cliff discipline).
19670            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19671            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19672            // outer row, so it rides the lateral channel. Same rule the unnest
19673            // arm uses.
19674            let tref = self.parse_table_fn_ref()?;
19675            let correlated = tref
19676                .table_fn_call
19677                .as_deref()
19678                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19679            return Ok(if correlated {
19680                Self::wrap_correlated_srf(tref)
19681            } else {
19682                tref
19683            });
19684        }
19685        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19686        // [, step])` set-returning source. Same shape as unnest:
19687        // detect at the head, parse the comma-separated arg list,
19688        // dispatch downstream through the engine's set-returning
19689        // path. Supports integer triplets (mailrs's `WITH row_no AS
19690        // (SELECT * FROM generate_series(1, N))` pattern) and
19691        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19692        // date-range iteration pattern, which pre-3.10 had no
19693        // direct equivalent in SPG).
19694        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19695            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19696        {
19697            self.advance(); // generate_series
19698            self.advance(); // (
19699            let mut args: Vec<Expr> = Vec::new();
19700            loop {
19701                args.push(self.parse_expr(0)?);
19702                if matches!(self.peek(), Token::Comma) {
19703                    self.advance();
19704                    continue;
19705                }
19706                break;
19707            }
19708            if !matches!(self.peek(), Token::RParen) {
19709                return Err(self.err(alloc::format!(
19710                    "expected ')' after generate_series() arguments, got {:?}",
19711                    self.peek()
19712                )));
19713            }
19714            self.advance();
19715            if args.len() < 2 || args.len() > 3 {
19716                return Err(self.err(alloc::format!(
19717                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19718                    args.len()
19719                )));
19720            }
19721            let with_ordinality = self.absorb_with_ordinality();
19722            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19723            let name = alias_ident
19724                .clone()
19725                .unwrap_or_else(|| "generate_series".to_string());
19726            let correlated = args.iter().any(Self::expr_has_any_column);
19727            let tref = TableRef {
19728                name,
19729                alias: alias_ident,
19730                only: false,
19731                as_of_segment: None,
19732                unnest_expr: None,
19733                unnest_column_aliases: column_aliases,
19734                with_ordinality,
19735                generate_series_args: Some(args),
19736                lateral_subquery: None,
19737                jsonb_each_text_arg: None,
19738                table_fn_call: None,
19739                rows_from: None,
19740                json_table: None,
19741                scalar_fn_item: false,
19742            };
19743            return Ok(if correlated {
19744                Self::wrap_correlated_srf(tref)
19745            } else {
19746                tref
19747            });
19748        }
19749        // v7.16.2 — preserve information_schema / pg_catalog
19750        // qualifiers (mailrs round-10 A.3). The generic
19751        // `expect_ident_like` strip silently drops the schema;
19752        // we want the engine to recognise these PG meta tables
19753        // and synthesise rows from the live catalog. Produce a
19754        // synthetic name (`__spg_info_columns` etc.) so the
19755        // engine's SELECT-side router can dispatch without
19756        // clashing with any user-defined `columns` table.
19757        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
19758            (synth, Some(orig))
19759        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
19760            (synth, Some(orig))
19761        } else {
19762            (self.expect_ident_like()?, None)
19763        };
19764        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19765        // time-travel clause. Parse BEFORE the alias so the
19766        // alias can still ride at the tail (`tbl AS OF SEGMENT
19767        // '5' alias`). `AS` is a reserved keyword token, while
19768        // `OF` and `SEGMENT` are bare idents.
19769        let as_of_segment = if matches!(self.peek(), Token::As)
19770            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19771        {
19772            self.advance(); // AS
19773            self.advance(); // OF
19774            let kw = match self.peek().clone() {
19775                Token::Ident(s) | Token::QuotedIdent(s) => s,
19776                other => {
19777                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19778                }
19779            };
19780            if !kw.eq_ignore_ascii_case("segment") {
19781                return Err(self.err(format!(
19782                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19783                )));
19784            }
19785            self.advance();
19786            // Segment id literal — accept either a string or
19787            // integer for operator ergonomics.
19788            let id = match self.advance() {
19789                Token::String(s) => s
19790                    .parse::<u32>()
19791                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19792                Token::Integer(n) => u32::try_from(n)
19793                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19794                other => {
19795                    return Err(self.err(format!(
19796                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
19797                    )));
19798                }
19799            };
19800            Some(id)
19801        } else {
19802            None
19803        };
19804        // TABLESAMPLE is not a reserved token — keep the bare-ident
19805        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19806        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19807        {
19808            None
19809        } else {
19810            self.parse_optional_alias()?
19811        };
19812        // r1052 — a catalog name rewritten to its synthetic form keeps
19813        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
19814        // still binds after `pg_cast` became `__spg_pg_cast`. PG
19815        // semantics: the visible name of `pg_catalog.pg_cast` IS
19816        // `pg_cast`. Without this, every table-name-qualified column
19817        // on a synthesised catalog answered "missing FROM-clause
19818        // entry" — which is the wall pg_dump hit on its first
19819        // pg_proc/pg_cast query.
19820        let alias = match (&alias, &meta_original) {
19821            (None, Some(orig)) if *orig != name => Some(orig.clone()),
19822            _ => alias,
19823        };
19824        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19825        // (PG grammar). BERNOULLI lowers to a per-row
19826        // `random() < p/100` conjunct on the enclosing SELECT's
19827        // WHERE — exact row-level Bernoulli semantics. SYSTEM
19828        // shares the lowering: SPG has no page structure to
19829        // sample, and the row-level form returns the same expected
19830        // fraction. REPEATABLE(seed) promises a deterministic
19831        // sample SPG cannot honour yet — honest error rather than
19832        // a silently ignored seed.
19833        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19834            self.advance();
19835            let method = self.expect_ident_like()?;
19836            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19837                return Err(self.err(alloc::format!(
19838                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19839                )));
19840            }
19841            if !matches!(self.peek(), Token::LParen) {
19842                return Err(self.err(alloc::format!(
19843                    "expected '(' after TABLESAMPLE {}, got {:?}",
19844                    method.to_ascii_uppercase(),
19845                    self.peek()
19846                )));
19847            }
19848            self.advance();
19849            let percent = self.parse_expr(0)?;
19850            if !matches!(self.peek(), Token::RParen) {
19851                return Err(self.err(alloc::format!(
19852                    "expected ')' after TABLESAMPLE percentage, got {:?}",
19853                    self.peek()
19854                )));
19855            }
19856            self.advance();
19857            // REPEATABLE(seed) → a deterministic per-row draw seeded by
19858            // `seed`, so the sample is stable across repeats and rescans.
19859            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19860            let mut sample_seed: Option<Expr> = None;
19861            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19862                self.advance();
19863                if !matches!(self.peek(), Token::LParen) {
19864                    return Err(self.err(alloc::format!(
19865                        "expected '(' after REPEATABLE, got {:?}",
19866                        self.peek()
19867                    )));
19868                }
19869                self.advance();
19870                let seed = self.parse_expr(0)?;
19871                if !matches!(self.peek(), Token::RParen) {
19872                    return Err(self.err(alloc::format!(
19873                        "expected ')' after REPEATABLE seed, got {:?}",
19874                        self.peek()
19875                    )));
19876                }
19877                self.advance();
19878                sample_seed = Some(seed);
19879            }
19880            let draw = match sample_seed {
19881                Some(seed) => Expr::FunctionCall {
19882                    name: "__tsm_fract".to_string(),
19883                    args: alloc::vec![seed],
19884                },
19885                None => Expr::FunctionCall {
19886                    name: "random".to_string(),
19887                    args: Vec::new(),
19888                },
19889            };
19890            self.pending_sample_preds.push(Expr::Binary {
19891                lhs: Box::new(draw),
19892                op: crate::ast::BinOp::Lt,
19893                rhs: Box::new(Expr::Binary {
19894                    lhs: Box::new(percent),
19895                    op: crate::ast::BinOp::Div,
19896                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19897                }),
19898            });
19899        }
19900        Ok(TableRef {
19901            name,
19902            alias,
19903            only,
19904            as_of_segment,
19905            unnest_expr: None,
19906            unnest_column_aliases: Vec::new(),
19907            with_ordinality: false,
19908            generate_series_args: None,
19909            lateral_subquery: None,
19910            jsonb_each_text_arg: None,
19911            table_fn_call: None,
19912            rows_from: None,
19913            json_table: None,
19914            scalar_fn_item: false,
19915        })
19916    }
19917
19918    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19919    /// but also accepts `AS alias(col [, col, …])` — the
19920    /// PG-standard table-function column-list form. The column
19921    /// list is only honoured when paired with `UNNEST(...)` in
19922    /// the parent; other call sites currently discard it.
19923    /// True when the expression tree contains a qualified column
19924    /// reference (`t.col`) — the syntactic marker that an SRF
19925    /// argument correlates with a preceding FROM item.
19926    fn expr_has_qualified_column(e: &Expr) -> bool {
19927        match e {
19928            Expr::Column(c) => c.qualifier.is_some(),
19929            Expr::Binary { lhs, rhs, .. } => {
19930                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19931            }
19932            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19933            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19934            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19935            Expr::Case {
19936                operand,
19937                branches,
19938                else_branch,
19939            } => {
19940                operand
19941                    .as_deref()
19942                    .is_some_and(Self::expr_has_qualified_column)
19943                    || branches.iter().any(|(w, t)| {
19944                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19945                    })
19946                    || else_branch
19947                        .as_deref()
19948                        .is_some_and(Self::expr_has_qualified_column)
19949            }
19950            _ => false,
19951        }
19952    }
19953
19954    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19955    /// counts a bare (unqualified) column. A set-returning function has no
19956    /// input columns of its own, so ANY column in its arguments is an outer
19957    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19958    fn expr_has_any_column(e: &Expr) -> bool {
19959        match e {
19960            Expr::Column(_) => true,
19961            Expr::Binary { lhs, rhs, .. } => {
19962                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19963            }
19964            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19965            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19966            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19967            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19968            // constructor or subscript fell to the `_ => false` arm, so
19969            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19970            // channel and the eager peer eval answered `column "x" does
19971            // not exist` (the substitution walker already recurses both
19972            // shapes; only this detector was blind to them).
19973            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19974            Expr::ArraySubscript { target, index } => {
19975                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19976            }
19977            Expr::Case {
19978                operand,
19979                branches,
19980                else_branch,
19981            } => {
19982                operand.as_deref().is_some_and(Self::expr_has_any_column)
19983                    || branches
19984                        .iter()
19985                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19986                    || else_branch
19987                        .as_deref()
19988                        .is_some_and(Self::expr_has_any_column)
19989            }
19990            _ => false,
19991        }
19992    }
19993
19994    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19995    /// `generate_series(1, t.n)`) into the lateral_subquery
19996    /// channel: `SELECT * FROM <srf>` executes per outer row with
19997    /// outer references substituted (v7.37.43-T4.5 machinery).
19998    /// Uncorrelated SRFs stay on their plain channels.
19999    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20000        let name = srf.name.clone();
20001        let alias = srf.alias.clone();
20002        let inner = crate::ast::SelectStatement {
20003            locking: None,
20004            ctes: Vec::new(),
20005            distinct: false,
20006            distinct_on: Vec::new(),
20007            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20008            from: Some(crate::ast::FromClause {
20009                primary: srf,
20010                joins: Vec::new(),
20011            }),
20012            where_: None,
20013            group_by: None,
20014            group_by_all: false,
20015            having: None,
20016            unions: Vec::new(),
20017            order_by: Vec::new(),
20018            limit: None,
20019            offset: None,
20020            limit_with_ties: false,
20021            window_check_exprs: Vec::new(),
20022        };
20023        TableRef {
20024            name,
20025            alias,
20026            only: false,
20027            as_of_segment: None,
20028            unnest_expr: None,
20029            unnest_column_aliases: Vec::new(),
20030            with_ordinality: false,
20031            generate_series_args: None,
20032            lateral_subquery: Some(Box::new(inner)),
20033            jsonb_each_text_arg: None,
20034            table_fn_call: None,
20035            rows_from: None,
20036            json_table: None,
20037            scalar_fn_item: false,
20038        }
20039    }
20040
20041    /// True when the expression tree contains an unresolved
20042    /// `OVER w` marker (see parse_over_clause).
20043    fn expr_has_named_window(e: &Expr) -> bool {
20044        match e {
20045            Expr::WindowFunction { partition_by, .. } => matches!(
20046                partition_by.as_slice(),
20047                [Expr::Column(c)] if matches!(
20048                    c.qualifier.as_deref(),
20049                    Some("__named_window__") | Some("__named_window_ref__")
20050                )
20051            ),
20052            Expr::Binary { lhs, rhs, .. } => {
20053                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20054            }
20055            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20056            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20057            Expr::Case {
20058                operand,
20059                branches,
20060                else_branch,
20061            } => {
20062                operand.as_deref().is_some_and(Self::expr_has_named_window)
20063                    || branches.iter().any(|(w, t)| {
20064                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20065                    })
20066                    || else_branch
20067                        .as_deref()
20068                        .is_some_and(Self::expr_has_named_window)
20069            }
20070            _ => false,
20071        }
20072    }
20073
20074    /// v7.39 (round 705) — the NAMES the expression references through the
20075    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20076    /// definitions nothing referenced. Traversal mirrors
20077    /// `expr_has_named_window` above.
20078    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20079        match e {
20080            Expr::WindowFunction { partition_by, .. } => {
20081                if let [Expr::Column(c)] = partition_by.as_slice()
20082                    && matches!(
20083                        c.qualifier.as_deref(),
20084                        Some("__named_window__") | Some("__named_window_ref__")
20085                    )
20086                {
20087                    into.push(c.name.clone());
20088                }
20089            }
20090            Expr::Binary { lhs, rhs, .. } => {
20091                Self::collect_named_window_refs(lhs, into);
20092                Self::collect_named_window_refs(rhs, into);
20093            }
20094            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20095                Self::collect_named_window_refs(expr, into);
20096            }
20097            Expr::FunctionCall { args, .. } => {
20098                for a in args {
20099                    Self::collect_named_window_refs(a, into);
20100                }
20101            }
20102            Expr::Case {
20103                operand,
20104                branches,
20105                else_branch,
20106            } => {
20107                if let Some(o) = operand.as_deref() {
20108                    Self::collect_named_window_refs(o, into);
20109                }
20110                for (w, t) in branches {
20111                    Self::collect_named_window_refs(w, into);
20112                    Self::collect_named_window_refs(t, into);
20113                }
20114                if let Some(eb) = else_branch.as_deref() {
20115                    Self::collect_named_window_refs(eb, into);
20116                }
20117            }
20118            _ => {}
20119        }
20120    }
20121
20122    /// Inline named-window definitions into the `OVER w` markers.
20123    /// An unknown name errors (PG: window "w" does not exist).
20124    #[allow(clippy::type_complexity)]
20125    fn substitute_named_windows(
20126        e: &mut Expr,
20127        defs: &[(
20128            String,
20129            (
20130                Vec<Expr>,
20131                Vec<(Expr, bool, Option<bool>)>,
20132                Option<WindowFrame>,
20133            ),
20134        )],
20135    ) -> Result<(), String> {
20136        match e {
20137            Expr::WindowFunction {
20138                partition_by,
20139                order_by,
20140                frame,
20141                ..
20142            } => {
20143                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20144                // from the bare `OVER w1` (a plain reference).
20145                let named = match partition_by.as_slice() {
20146                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20147                        Some("__named_window__") => Some((c.name.clone(), false)),
20148                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20149                        _ => None,
20150                    },
20151                    _ => None,
20152                };
20153                if let Some((wname, is_copy)) = named {
20154                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20155                    else {
20156                        return Err(alloc::format!("window {wname:?} does not exist"));
20157                    };
20158                    if !is_copy {
20159                        *partition_by = def.0.clone();
20160                        *order_by = def.1.clone();
20161                        *frame = def.2.clone();
20162                        return Ok(());
20163                    }
20164                    // v7.39 (round 229) — PG's copy rules, probed against
20165                    // 18.4: a copy inherits the partitioning, may supply an
20166                    // ordering only when the base has none, and may not copy
20167                    // a base that already carries a frame (its own frame
20168                    // would be ambiguous with the inherited one).
20169                    if !def.1.is_empty() && !order_by.is_empty() {
20170                        return Err(alloc::format!(
20171                            "cannot override ORDER BY clause of window \"{wname}\""
20172                        ));
20173                    }
20174                    if def.2.is_some() {
20175                        return Err(alloc::format!(
20176                            "cannot copy window \"{wname}\" because it has a frame clause"
20177                        ));
20178                    }
20179                    *partition_by = def.0.clone();
20180                    if order_by.is_empty() {
20181                        *order_by = def.1.clone();
20182                    }
20183                }
20184                Ok(())
20185            }
20186            Expr::Binary { lhs, rhs, .. } => {
20187                Self::substitute_named_windows(lhs, defs)?;
20188                Self::substitute_named_windows(rhs, defs)
20189            }
20190            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20191                Self::substitute_named_windows(expr, defs)
20192            }
20193            Expr::FunctionCall { args, .. } => {
20194                for a in args {
20195                    Self::substitute_named_windows(a, defs)?;
20196                }
20197                Ok(())
20198            }
20199            Expr::Case {
20200                operand,
20201                branches,
20202                else_branch,
20203            } => {
20204                if let Some(op) = operand {
20205                    Self::substitute_named_windows(op, defs)?;
20206                }
20207                for (w, t) in branches {
20208                    Self::substitute_named_windows(w, defs)?;
20209                    Self::substitute_named_windows(t, defs)?;
20210                }
20211                if let Some(el) = else_branch {
20212                    Self::substitute_named_windows(el, defs)?;
20213                }
20214                Ok(())
20215            }
20216            _ => Ok(()),
20217        }
20218    }
20219
20220    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20221    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20222    /// composition.
20223    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20224        debug_assert!(matches!(self.peek(), Token::Table));
20225        self.advance(); // TABLE
20226        let tname = self.expect_ident_like()?;
20227        Ok(SelectStatement {
20228            locking: None,
20229            ctes: Vec::new(),
20230            distinct: false,
20231            distinct_on: Vec::new(),
20232            items: alloc::vec![SelectItem::Wildcard],
20233            from: Some(FromClause {
20234                primary: TableRef {
20235                    name: tname,
20236                    alias: None,
20237                    only: false,
20238                    as_of_segment: None,
20239                    unnest_expr: None,
20240                    unnest_column_aliases: Vec::new(),
20241                    with_ordinality: false,
20242                    generate_series_args: None,
20243                    lateral_subquery: None,
20244                    jsonb_each_text_arg: None,
20245                    table_fn_call: None,
20246                    rows_from: None,
20247                    json_table: None,
20248                    scalar_fn_item: false,
20249                },
20250                joins: Vec::new(),
20251            }),
20252            where_: None,
20253            group_by: None,
20254            group_by_all: false,
20255            having: None,
20256            unions: Vec::new(),
20257            order_by: Vec::new(),
20258            limit: None,
20259            offset: None,
20260            limit_with_ties: false,
20261            window_check_exprs: Vec::new(),
20262        })
20263    }
20264
20265    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20266    /// variants) → a derived table that reads each declared column out of
20267    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20268    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20269    /// the scalar *record form projects a single row straight off `J`.
20270    /// Rides the existing lateral-subquery channel, so no new executor or
20271    /// AST is needed.
20272    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20273        use crate::ast::{
20274            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20275        };
20276        let fn_name = match self.peek() {
20277            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20278            _ => unreachable!("caller guarded is_json_to_record_name"),
20279        };
20280        self.advance(); // fn name
20281        self.advance(); // (
20282        let mut arg = self.parse_expr(0)?;
20283        // populate_record(base, json): the base only carries the record
20284        // type here — the JSON argument is the second expression.
20285        let mut base: Option<Expr> = None;
20286        if matches!(self.peek(), Token::Comma) {
20287            self.advance();
20288            base = Some(arg);
20289            arg = self.parse_expr(0)?;
20290        }
20291        if !matches!(self.peek(), Token::RParen) {
20292            return Err(self.err(alloc::format!(
20293                "expected ')' after {fn_name}() argument, got {:?}",
20294                self.peek()
20295            )));
20296        }
20297        self.advance(); // )
20298        let is_set = fn_name.ends_with("recordset");
20299        // `[AS] alias ( col type [, …] )` column-definition list.
20300        if matches!(self.peek(), Token::As) {
20301            self.advance();
20302        }
20303        let alias_opt = match self.peek() {
20304            Token::Ident(s) | Token::QuotedIdent(s) => {
20305                let a = s.clone();
20306                self.advance();
20307                Some(a)
20308            }
20309            _ => None,
20310        };
20311        // v7.39 (read01 round 76) — the populate family's canonical PG
20312        // spelling carries no column list at all: the row shape comes from
20313        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20314        // j)`). The parser has no catalog, so hand the two arguments to the
20315        // engine's table-function channel, which does. Only `*_to_record*`
20316        // (whose base is bare `record`) genuinely requires the list.
20317        if !matches!(self.peek(), Token::LParen) {
20318            if let Some(base_expr) = base {
20319                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20320                return Ok(TableRef {
20321                    name: alias.clone(),
20322                    alias: Some(alias),
20323                    only: false,
20324                    as_of_segment: None,
20325                    unnest_expr: None,
20326                    unnest_column_aliases: Vec::new(),
20327                    with_ordinality: false,
20328                    generate_series_args: None,
20329                    lateral_subquery: None,
20330                    jsonb_each_text_arg: None,
20331                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20332                    rows_from: None,
20333                    json_table: None,
20334                    scalar_fn_item: false,
20335                });
20336            }
20337            return Err(self.err(alloc::format!(
20338                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20339                self.peek()
20340            )));
20341        }
20342        let Some(alias) = alias_opt else {
20343            return Err(self.err(alloc::format!(
20344                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20345            )));
20346        };
20347        self.advance(); // (
20348        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20349        loop {
20350            let col = self.expect_ident_like()?;
20351            let ty = self.parse_cast_target()?;
20352            coldefs.push((col, ty));
20353            if matches!(self.peek(), Token::Comma) {
20354                self.advance();
20355                continue;
20356            }
20357            if matches!(self.peek(), Token::RParen) {
20358                self.advance();
20359                break;
20360            }
20361            return Err(self.err(alloc::format!(
20362                "expected ',' or ')' in {fn_name} column list, got {:?}",
20363                self.peek()
20364            )));
20365        }
20366        if coldefs.is_empty() {
20367            return Err(self.err(alloc::format!(
20368                "{fn_name} column-definition list must declare at least one column"
20369            )));
20370        }
20371        // Per column: (base ->> 'col')::type AS col. The base is the
20372        // per-element `value` column for the *set form, or the argument
20373        // itself for the scalar record form.
20374        let items: Vec<SelectItem> = coldefs
20375            .into_iter()
20376            .map(|(col, ty)| {
20377                let base = if is_set {
20378                    Expr::Column(ColumnName {
20379                        qualifier: None,
20380                        name: "value".to_string(),
20381                    })
20382                } else {
20383                    arg.clone()
20384                };
20385                SelectItem::Expr {
20386                    expr: Expr::Cast {
20387                        expr: Box::new(Expr::Binary {
20388                            lhs: Box::new(base),
20389                            op: BinOp::JsonGetText,
20390                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20391                        }),
20392                        target: ty,
20393                    },
20394                    alias: Some(col),
20395                }
20396            })
20397            .collect();
20398        let from = if is_set {
20399            let elem_fn = if fn_name.starts_with("jsonb") {
20400                "jsonb_array_elements"
20401            } else {
20402                "json_array_elements"
20403            };
20404            Some(FromClause {
20405                primary: TableRef {
20406                    name: "value".to_string(),
20407                    alias: None,
20408                    only: false,
20409                    as_of_segment: None,
20410                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20411                        name: elem_fn.to_string(),
20412                        args: alloc::vec![arg],
20413                    })),
20414                    unnest_column_aliases: alloc::vec!["value".to_string()],
20415                    with_ordinality: false,
20416                    generate_series_args: None,
20417                    lateral_subquery: None,
20418                    jsonb_each_text_arg: None,
20419                    table_fn_call: None,
20420                    rows_from: None,
20421                    json_table: None,
20422                    scalar_fn_item: false,
20423                },
20424                joins: Vec::new(),
20425            })
20426        } else {
20427            None
20428        };
20429        let inner = SelectStatement {
20430            locking: None,
20431            ctes: Vec::new(),
20432            distinct: false,
20433            distinct_on: Vec::new(),
20434            items,
20435            from,
20436            where_: None,
20437            group_by: None,
20438            group_by_all: false,
20439            having: None,
20440            unions: Vec::new(),
20441            order_by: Vec::new(),
20442            limit: None,
20443            offset: None,
20444            limit_with_ties: false,
20445            window_check_exprs: Vec::new(),
20446        };
20447        Ok(TableRef {
20448            name: alias.clone(),
20449            alias: Some(alias),
20450            only: false,
20451            as_of_segment: None,
20452            unnest_expr: None,
20453            unnest_column_aliases: Vec::new(),
20454            with_ordinality: false,
20455            generate_series_args: None,
20456            lateral_subquery: Some(Box::new(inner)),
20457            jsonb_each_text_arg: None,
20458            table_fn_call: None,
20459            rows_from: None,
20460            json_table: None,
20461            scalar_fn_item: false,
20462        })
20463    }
20464
20465    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20466    /// Returns true when the clause was present. `WITH` alone (a
20467    /// CTE can never start here) is not enough — the ORDINALITY
20468    /// ident must follow, so a stray WITH still errors downstream.
20469    fn absorb_with_ordinality(&mut self) -> bool {
20470        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20471            && matches!(self.tokens.get(self.pos + 1),
20472                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20473        {
20474            self.advance();
20475            self.advance();
20476            true
20477        } else {
20478            false
20479        }
20480    }
20481
20482    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20483    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20484    /// Out-of-line: the caller sits on the FROM recursion chain.
20485    #[inline(never)]
20486    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20487        let fn_name = match self.advance() {
20488            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20489            _ => unreachable!("caller peeked an ident"),
20490        };
20491        self.advance(); // (
20492        let mut args: Vec<Expr> = Vec::new();
20493        if !matches!(self.peek(), Token::RParen) {
20494            loop {
20495                args.push(self.parse_expr(0)?);
20496                if matches!(self.peek(), Token::Comma) {
20497                    self.advance();
20498                    continue;
20499                }
20500                break;
20501            }
20502        }
20503        if !matches!(self.peek(), Token::RParen) {
20504            return Err(self.err(alloc::format!(
20505                "expected ')' after {fn_name}() arguments, got {:?}",
20506                self.peek()
20507            )));
20508        }
20509        self.advance();
20510        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20511        // counter column rides after the function's own, and the alias list
20512        // names it.
20513        let with_ordinality = self.absorb_with_ordinality();
20514        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20515        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20516        Ok(TableRef {
20517            name,
20518            alias: alias_ident,
20519            only: false,
20520            as_of_segment: None,
20521            unnest_expr: None,
20522            unnest_column_aliases,
20523            with_ordinality,
20524            generate_series_args: None,
20525            lateral_subquery: None,
20526            jsonb_each_text_arg: None,
20527            table_fn_call: Some(Box::new((fn_name, args))),
20528            rows_from: None,
20529            json_table: None,
20530            scalar_fn_item: false,
20531        })
20532    }
20533
20534    /// v7.39 (round 205, JSON_TABLE) — parse
20535    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20536    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20537    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20538    #[inline(never)]
20539    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20540        self.advance(); // json_table
20541        self.advance(); // (
20542        let doc = Box::new(self.parse_expr(0)?);
20543        self.expect_comma_json_table()?;
20544        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20545        // Optional `PASSING <expr> AS <name> [, …]`.
20546        let mut passing: Vec<(String, Expr)> = Vec::new();
20547        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20548            self.advance();
20549            loop {
20550                let e = self.parse_expr(0)?;
20551                if !matches!(self.peek(), Token::As) {
20552                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20553                }
20554                self.advance();
20555                let vname = match self.advance() {
20556                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20557                    other => {
20558                        return Err(self.err(alloc::format!(
20559                            "expected PASSING variable name, got {other:?}"
20560                        )));
20561                    }
20562                };
20563                passing.push((vname, e));
20564                if matches!(self.peek(), Token::Comma) {
20565                    self.advance();
20566                    continue;
20567                }
20568                break;
20569            }
20570        }
20571        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20572            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20573        }
20574        self.advance();
20575        let columns = self.parse_json_table_columns()?;
20576        if !matches!(self.peek(), Token::RParen) {
20577            return Err(self.err(alloc::format!(
20578                "expected ')' to close JSON_TABLE, got {:?}",
20579                self.peek()
20580            )));
20581        }
20582        self.advance();
20583        let alias_ident = self.parse_optional_alias()?;
20584        let name = alias_ident
20585            .clone()
20586            .unwrap_or_else(|| String::from("json_table"));
20587        Ok(TableRef {
20588            name,
20589            alias: alias_ident,
20590            only: false,
20591            as_of_segment: None,
20592            unnest_expr: None,
20593            unnest_column_aliases: Vec::new(),
20594            with_ordinality: false,
20595            generate_series_args: None,
20596            lateral_subquery: None,
20597            jsonb_each_text_arg: None,
20598            table_fn_call: None,
20599            rows_from: None,
20600            json_table: Some(Box::new(crate::ast::JsonTable {
20601                doc,
20602                row_path,
20603                columns,
20604                passing,
20605            })),
20606            scalar_fn_item: false,
20607        })
20608    }
20609
20610    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20611        if !matches!(self.peek(), Token::Comma) {
20612            return Err(self.err(alloc::format!(
20613                "expected ',' after JSON_TABLE document, got {:?}",
20614                self.peek()
20615            )));
20616        }
20617        self.advance();
20618        Ok(())
20619    }
20620
20621    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20622        match self.advance() {
20623            Token::String(s) => Ok(s),
20624            other => Err(self.err(alloc::format!(
20625                "expected {what} string literal, got {other:?}"
20626            ))),
20627        }
20628    }
20629
20630    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20631    #[inline(never)]
20632    fn parse_json_table_columns(
20633        &mut self,
20634    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20635        if !matches!(self.peek(), Token::LParen) {
20636            return Err(self.err("expected '(' after COLUMNS".into()));
20637        }
20638        self.advance();
20639        let mut cols = Vec::new();
20640        loop {
20641            cols.push(self.parse_json_table_one_column()?);
20642            if matches!(self.peek(), Token::Comma) {
20643                self.advance();
20644                continue;
20645            }
20646            break;
20647        }
20648        if !matches!(self.peek(), Token::RParen) {
20649            return Err(self.err(alloc::format!(
20650                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20651                self.peek()
20652            )));
20653        }
20654        self.advance();
20655        Ok(cols)
20656    }
20657
20658    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20659        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20660        // NESTED [PATH] '<p>' COLUMNS (...)
20661        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20662            self.advance();
20663            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20664                self.advance();
20665            }
20666            let path = self.parse_json_string_literal("NESTED PATH")?;
20667            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20668                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20669            }
20670            self.advance();
20671            let columns = self.parse_json_table_columns()?;
20672            return Ok(JsonTableColumn::Nested { path, columns });
20673        }
20674        // <name> ...
20675        let name = match self.advance() {
20676            Token::Ident(s) | Token::QuotedIdent(s) => s,
20677            other => {
20678                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20679            }
20680        };
20681        // <name> FOR ORDINALITY
20682        if matches!(self.peek(), Token::For) {
20683            self.advance();
20684            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20685                return Err(self.err("expected ORDINALITY after FOR".into()));
20686            }
20687            self.advance();
20688            return Ok(JsonTableColumn::Ordinality { name });
20689        }
20690        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20691        let ty = self.parse_column_type_name()?;
20692        let mut format_json = false;
20693        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20694            self.advance();
20695            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20696                return Err(self.err("expected JSON after FORMAT".into()));
20697            }
20698            self.advance();
20699            format_json = true;
20700        }
20701        let mut exists = false;
20702        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20703            self.advance();
20704            exists = true;
20705        }
20706        let mut path = alloc::format!("$.{name}");
20707        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20708            self.advance();
20709            path = self.parse_json_string_literal("column PATH")?;
20710        }
20711        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20712            // `FORMAT JSON` after PATH (alternate placement).
20713            self.advance();
20714            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20715                self.advance();
20716            }
20717            format_json = true;
20718        }
20719        let mut wrapper = false;
20720        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20721            self.advance();
20722            // optional CONDITIONAL/UNCONDITIONAL
20723            if matches!(self.peek(), Token::Ident(s)
20724                if s.eq_ignore_ascii_case("unconditional")
20725                    || s.eq_ignore_ascii_case("conditional"))
20726            {
20727                self.advance();
20728            }
20729            if !matches!(self.peek(), Token::Ident(s)
20730                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20731            {
20732                return Err(self.err("expected WRAPPER after WITH".into()));
20733            }
20734            self.advance();
20735            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20736            if matches!(self.peek(), Token::Ident(s)
20737                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20738            {
20739                self.advance();
20740            }
20741            wrapper = true;
20742        }
20743        // ON EMPTY / ON ERROR clauses (two, in any order).
20744        let mut on_empty = JsonTableOnBehavior::Null;
20745        let mut on_error = JsonTableOnBehavior::Null;
20746        for _ in 0..2 {
20747            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20748            {
20749                self.advance();
20750                Some(JsonTableOnBehavior::Error)
20751            } else if matches!(self.peek(), Token::Null) {
20752                self.advance();
20753                Some(JsonTableOnBehavior::Null)
20754            } else if matches!(self.peek(), Token::Default) {
20755                self.advance();
20756                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20757            } else {
20758                None
20759            };
20760            let Some(behavior) = behavior else { break };
20761            // `ON {EMPTY|ERROR}`
20762            if !matches!(self.peek(), Token::On) {
20763                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20764            }
20765            self.advance();
20766            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20767                self.advance();
20768                on_empty = behavior;
20769            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20770                self.advance();
20771                on_error = behavior;
20772            } else {
20773                return Err(self.err("expected EMPTY or ERROR after ON".into()));
20774            }
20775        }
20776        Ok(JsonTableColumn::Regular {
20777            name,
20778            ty,
20779            path,
20780            exists,
20781            format_json,
20782            wrapper,
20783            on_empty,
20784            on_error,
20785        })
20786    }
20787
20788    fn parse_optional_alias_with_columns(
20789        &mut self,
20790    ) -> Result<(Option<String>, Vec<String>), ParseError> {
20791        let alias = self.parse_optional_alias()?;
20792        if alias.is_none() {
20793            return Ok((None, Vec::new()));
20794        }
20795        let mut cols: Vec<String> = Vec::new();
20796        if matches!(self.peek(), Token::LParen) {
20797            self.advance();
20798            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20799                self.advance();
20800                cols.push(s);
20801                if matches!(self.peek(), Token::Comma) {
20802                    self.advance();
20803                    continue;
20804                }
20805                break;
20806            }
20807            if matches!(self.peek(), Token::RParen) {
20808                self.advance();
20809            }
20810        }
20811        Ok((alias, cols))
20812    }
20813
20814    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20815    /// whose keyword token was already consumed and whose `(` is the
20816    /// current token. Factored out of `parse_atom` (and marked
20817    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20818    /// recursive `parse_atom` frame — inlining them there enlarges the
20819    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20820    /// against, risking an overflow before the budget triggers.
20821    #[inline(never)]
20822    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20823        self.advance(); // (
20824        let mut args = Vec::new();
20825        if !matches!(self.peek(), Token::RParen) {
20826            loop {
20827                args.push(self.parse_expr(0)?);
20828                match self.peek() {
20829                    Token::Comma => {
20830                        self.advance();
20831                    }
20832                    Token::RParen => break,
20833                    other => {
20834                        return Err(self.err(alloc::format!(
20835                            "expected ',' or ')' in {name}() args, got {other:?}"
20836                        )));
20837                    }
20838                }
20839            }
20840        }
20841        self.advance(); // )
20842        Ok(Expr::FunctionCall {
20843            name: name.into(),
20844            args,
20845        })
20846    }
20847
20848    /// FROM-clause: a primary table reference plus zero-or-more joined
20849    /// peers expressed via either `, <table>` (cross-product, no ON) or
20850    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20851    /// v1.10 keeps the join list flat (left-associative nested-loop
20852    /// semantics).
20853    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20854        let primary = self.parse_table_ref()?;
20855        let primary_qual = primary
20856            .alias
20857            .clone()
20858            .unwrap_or_else(|| primary.name.clone());
20859        let joins = self.parse_from_joins(&primary_qual)?;
20860        Ok(FromClause { primary, joins })
20861    }
20862
20863    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20864    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20865    /// SAME grammar after its target table has already been consumed.
20866    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20867    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20868    /// be parsed forward, once.)
20869    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20870    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20871    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20872    /// desugaring, which needs a name for the left side of each equality.
20873    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20874        let mut joins = Vec::new();
20875        loop {
20876            // `, <table>` — cross-product with no ON.
20877            if matches!(self.peek(), Token::Comma) {
20878                self.advance();
20879                let table = self.parse_table_ref()?;
20880                joins.push(FromJoin {
20881                    kind: JoinKind::Cross,
20882                    table,
20883                    on: None,
20884                    using_cols: None,
20885                    natural: false,
20886                });
20887                continue;
20888            }
20889            // v7.37.16 — optional leading `NATURAL` before the join
20890            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20891            // not a lexer keyword (it arrives as a bare Ident), so match
20892            // it case-insensitively here. When present, no ON/USING
20893            // clause is allowed — the common columns are resolved at
20894            // execution time.
20895            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20896            if natural {
20897                self.advance();
20898            }
20899            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20900            // CROSS JOIN, and bare JOIN (defaults to INNER).
20901            let kind =
20902                match self.peek() {
20903                    Token::Inner => {
20904                        self.advance();
20905                        if !matches!(self.peek(), Token::Join) {
20906                            return Err(self
20907                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20908                        }
20909                        self.advance();
20910                        JoinKind::Inner
20911                    }
20912                    Token::Left => {
20913                        self.advance();
20914                        if matches!(self.peek(), Token::Outer) {
20915                            self.advance();
20916                        }
20917                        if !matches!(self.peek(), Token::Join) {
20918                            return Err(self.err(format!(
20919                                "expected JOIN after LEFT [OUTER], got {:?}",
20920                                self.peek()
20921                            )));
20922                        }
20923                        self.advance();
20924                        JoinKind::Left
20925                    }
20926                    Token::Cross => {
20927                        self.advance();
20928                        if !matches!(self.peek(), Token::Join) {
20929                            return Err(self
20930                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20931                        }
20932                        self.advance();
20933                        JoinKind::Cross
20934                    }
20935                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20936                    Token::Right => {
20937                        self.advance();
20938                        if matches!(self.peek(), Token::Outer) {
20939                            self.advance();
20940                        }
20941                        if !matches!(self.peek(), Token::Join) {
20942                            return Err(self.err(format!(
20943                                "expected JOIN after RIGHT [OUTER], got {:?}",
20944                                self.peek()
20945                            )));
20946                        }
20947                        self.advance();
20948                        JoinKind::Right
20949                    }
20950                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20951                    Token::Full => {
20952                        self.advance();
20953                        if matches!(self.peek(), Token::Outer) {
20954                            self.advance();
20955                        }
20956                        if !matches!(self.peek(), Token::Join) {
20957                            return Err(self.err(format!(
20958                                "expected JOIN after FULL [OUTER], got {:?}",
20959                                self.peek()
20960                            )));
20961                        }
20962                        self.advance();
20963                        JoinKind::FullOuter
20964                    }
20965                    Token::Join => {
20966                        self.advance();
20967                        JoinKind::Inner
20968                    }
20969                    _ => break,
20970                };
20971            let table = self.parse_table_ref()?;
20972            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20973            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20974            // where prev_table is the most-recent left-side table
20975            // (the previous join's table if any, else the FROM primary).
20976            // PG semantics around column merging are richer (USING'd
20977            // cols become deduplicated single output columns); for
20978            // sugar purposes the predicate-only form covers the
20979            // baseline corpus shape and chained `… JOIN x USING (k)
20980            // JOIN y USING (k)` calls.
20981            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20982            // common columns resolve at execution time.
20983            if natural {
20984                joins.push(FromJoin {
20985                    kind,
20986                    table,
20987                    on: None,
20988                    using_cols: None,
20989                    natural: true,
20990                });
20991                continue;
20992            }
20993            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20994            // v7.37.16 — capture the USING column list (in addition to
20995            // the ON desugar below) so the executor can perform PG's
20996            // column-merge on the output side.
20997            let mut using_cols: Option<Vec<String>> = None;
20998            let on = if matches!(self.peek(), Token::On) {
20999                self.advance();
21000                Some(self.parse_expr(0)?)
21001            } else if using_match {
21002                self.advance();
21003                if !matches!(self.peek(), Token::LParen) {
21004                    return Err(
21005                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21006                    );
21007                }
21008                self.advance();
21009                let mut cols: Vec<String> = Vec::new();
21010                loop {
21011                    match self.peek().clone() {
21012                        Token::Ident(s) | Token::QuotedIdent(s) => {
21013                            self.advance();
21014                            cols.push(s);
21015                        }
21016                        other => {
21017                            return Err(self.err(format!(
21018                                "expected column name inside USING (…), got {other:?}"
21019                            )));
21020                        }
21021                    }
21022                    match self.peek() {
21023                        Token::Comma => {
21024                            self.advance();
21025                            continue;
21026                        }
21027                        Token::RParen => {
21028                            self.advance();
21029                            break;
21030                        }
21031                        other => {
21032                            return Err(self.err(format!(
21033                                "expected ',' or ')' inside USING (…), got {other:?}"
21034                            )));
21035                        }
21036                    }
21037                }
21038                if cols.is_empty() {
21039                    return Err(self.err("USING (…) requires at least one column".to_string()));
21040                }
21041                using_cols = Some(cols.clone());
21042                // Pick the left-side alias: prev join's table if any,
21043                // else FROM primary. Use alias when present, else
21044                // table name (PG-equivalent qualifier).
21045                let left_qual: String = joins
21046                    .last()
21047                    .map(|j| {
21048                        j.table
21049                            .alias
21050                            .clone()
21051                            .unwrap_or_else(|| j.table.name.clone())
21052                    })
21053                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21054                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21055                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21056                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21057                        qualifier: Some(left_qual.clone()),
21058                        name: c.clone(),
21059                    })),
21060                    op: crate::ast::BinOp::Eq,
21061                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21062                        qualifier: Some(right_qual.clone()),
21063                        name: c,
21064                    })),
21065                });
21066                let first = iter.next().expect("at least one col");
21067                Some(iter.fold(first, |acc, pred| Expr::Binary {
21068                    lhs: alloc::boxed::Box::new(acc),
21069                    op: crate::ast::BinOp::And,
21070                    rhs: alloc::boxed::Box::new(pred),
21071                }))
21072            } else if kind == JoinKind::Cross {
21073                None
21074            } else {
21075                return Err(self.err(format!(
21076                    "expected ON or USING after {:?} JOIN, got {:?}",
21077                    kind,
21078                    self.peek()
21079                )));
21080            };
21081            joins.push(FromJoin {
21082                kind,
21083                table,
21084                on,
21085                using_cols,
21086                natural: false,
21087            });
21088        }
21089        Ok(joins)
21090    }
21091
21092    /// Optional alias after an expression or table:
21093    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21094    /// accepted (PG-style implicit alias). Returns `None` if the next token
21095    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21096    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21097        if matches!(self.peek(), Token::As) {
21098            self.advance();
21099            // v7.39 (round 340, V56) — after AS the next token MUST be an
21100            // identifier. This used to return None and "let the caller
21101            // surface the error on the next expectation", but when AS is
21102            // the LAST token there is no next expectation: `SELECT 1 AS`
21103            // parsed clean and silently dropped the alias. PG rejects it.
21104            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21105                return self.expect_ident_like().map(Some);
21106            }
21107            return Err(self.err(alloc::format!(
21108                "expected an alias after AS, got {:?}",
21109                self.peek()
21110            )));
21111        }
21112        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21113        // grammar reserves a long list of follow-keywords from the
21114        // alias slot. SPG's bareword approximation: skip a small
21115        // set of idents that would otherwise be swallowed as the
21116        // table alias and break trailing clauses like CREATE
21117        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21118        // CONFLICT WHERE shapes.
21119        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21120            if is_alias_stopword(s) {
21121                return Ok(None);
21122            }
21123            return Ok(self.expect_ident_like().ok());
21124        }
21125        Ok(None)
21126    }
21127
21128    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21129    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21130        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21131        // error beats a stack overflow (an overflow aborts the
21132        // embedding host process).
21133        self.enter_nested()?;
21134        let r = self.parse_expr_inner(min_prec);
21135        self.nest_depth -= 1;
21136        r
21137    }
21138
21139    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21140    /// When the upcoming tokens form one, return the underlying
21141    /// operator token and the position just past the closing paren
21142    /// so the binary loop can dispatch on the plain operator.
21143    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21144        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21145            return None;
21146        }
21147        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21148            return None;
21149        }
21150        let mut i = self.pos + 2;
21151        // Optional schema qualifier (pg_catalog.<op> etc.).
21152        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21153            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21154        {
21155            i += 2;
21156        }
21157        let op_tok = self.tokens.get(i)?.clone();
21158        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21159            return None;
21160        }
21161        Some((i + 2, op_tok))
21162    }
21163
21164    /// PG operator symbols that lower onto function calls in
21165    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21166    /// family → regexp_like, comparison rung), `^@` (starts_with,
21167    /// comparison rung), `^` (power, tighter than `*`), `#`
21168    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21169    /// subset of the OR bits so the subtraction never borrows).
21170    fn try_symbol_operator(
21171        &mut self,
21172        lhs: &Expr,
21173        min_prec: u8,
21174    ) -> Result<Option<Expr>, ParseError> {
21175        enum Sym {
21176            Regex { ci: bool, negated: bool },
21177            Like { ci: bool, negated: bool },
21178            StartsWith,
21179            Power,
21180            Xor,
21181            RangeAdjacent,
21182        }
21183        // v7.39 (IS-precedence knife) — the low-precedence postfix
21184        // predicates ride this existing leaf call (zero new frame slots
21185        // on the nesting chain).
21186        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21187            return Ok(Some(e));
21188        }
21189        let (sym, prec): (Sym, u8) = match self.peek() {
21190            Token::Tilde => (
21191                Sym::Regex {
21192                    ci: false,
21193                    negated: false,
21194                },
21195                5,
21196            ),
21197            Token::TildeStar => (
21198                Sym::Regex {
21199                    ci: true,
21200                    negated: false,
21201                },
21202                5,
21203            ),
21204            Token::NotTilde => (
21205                Sym::Regex {
21206                    ci: false,
21207                    negated: true,
21208                },
21209                5,
21210            ),
21211            Token::NotTildeStar => (
21212                Sym::Regex {
21213                    ci: true,
21214                    negated: true,
21215                },
21216                5,
21217            ),
21218            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21219            Token::DoubleTilde => (
21220                Sym::Like {
21221                    ci: false,
21222                    negated: false,
21223                },
21224                5,
21225            ),
21226            Token::DoubleTildeStar => (
21227                Sym::Like {
21228                    ci: true,
21229                    negated: false,
21230                },
21231                5,
21232            ),
21233            Token::NotDoubleTilde => (
21234                Sym::Like {
21235                    ci: false,
21236                    negated: true,
21237                },
21238                5,
21239            ),
21240            Token::NotDoubleTildeStar => (
21241                Sym::Like {
21242                    ci: true,
21243                    negated: true,
21244                },
21245                5,
21246            ),
21247            Token::CaretAt => (Sym::StartsWith, 5),
21248            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21249            // tighter than `* / & |`, which the prec-9 rung preserves —
21250            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21251            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21252            Token::Caret => (Sym::Power, 9),
21253            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21254            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21255            Token::Hash => (Sym::Xor, 6),
21256            Token::Adjacent => (Sym::RangeAdjacent, 5),
21257            _ => return Ok(None),
21258        };
21259        if prec < min_prec {
21260            return Ok(None);
21261        }
21262        self.advance();
21263        let rhs = self.parse_expr(prec + 1)?;
21264        let out = match sym {
21265            Sym::Regex { ci, negated } => {
21266                let mut args = alloc::vec![lhs.clone(), rhs];
21267                if ci {
21268                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21269                }
21270                maybe_not(
21271                    Expr::FunctionCall {
21272                        name: String::from("regexp_like"),
21273                        args,
21274                    },
21275                    negated,
21276                )
21277            }
21278            Sym::Like { ci, negated } => Expr::Like {
21279                expr: alloc::boxed::Box::new(lhs.clone()),
21280                pattern: alloc::boxed::Box::new(rhs),
21281                negated,
21282                case_insensitive: ci,
21283            },
21284            Sym::StartsWith => Expr::FunctionCall {
21285                name: String::from("starts_with"),
21286                args: alloc::vec![lhs.clone(), rhs],
21287            },
21288            Sym::Power => Expr::FunctionCall {
21289                name: String::from("power"),
21290                args: alloc::vec![lhs.clone(), rhs],
21291            },
21292            // `#` bitwise XOR — a real operator now (was desugared to
21293            // `(a|b)-(a&b)`, algebraically identical for integers but
21294            // undefined for bit strings; the direct op handles both).
21295            Sym::Xor => Expr::Binary {
21296                lhs: Box::new(lhs.clone()),
21297                op: BinOp::BitXor,
21298                rhs: Box::new(rhs),
21299            },
21300            // range `-|-` "is adjacent to" — lowered to a catalog function.
21301            Sym::RangeAdjacent => Expr::FunctionCall {
21302                name: String::from("range_adjacent"),
21303                args: alloc::vec![lhs.clone(), rhs],
21304            },
21305        };
21306        Ok(Some(out))
21307    }
21308
21309    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21310    /// predicates, moved out of the tight postfix-cast loop: PG binds
21311    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21312    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21313    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21314    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21315    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21316    /// when nothing at this position belongs to the family. Out-of-line
21317    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21318    /// chain that MAX_NEST_DEPTH is tuned against.
21319    #[inline(never)]
21320    fn parse_postfix_predicate(
21321        &mut self,
21322        lhs: &Expr,
21323        min_prec: u8,
21324    ) -> Result<Option<Expr>, ParseError> {
21325        // Reached through try_symbol_operator (an existing leaf call of
21326        // the binary loop) so NO new stack slots land on the per-nesting
21327        // frame chain; the lhs clones only when a predicate actually
21328        // consumes it.
21329        match self.peek() {
21330            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21331            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21332            Token::Is if min_prec <= 4 => {}
21333            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21334            Token::Not
21335                if min_prec <= 5
21336                    && matches!(
21337                        self.tokens.get(self.pos + 1),
21338                        Some(Token::Between | Token::In | Token::Like)
21339                    ) => {}
21340            Token::Not | Token::Ident(_)
21341                if min_prec <= 5
21342                    && (matches!(self.peek(), Token::Ident(s)
21343                            if s.eq_ignore_ascii_case("ilike")
21344                                || (self.mysql_dialect
21345                                    && (s.eq_ignore_ascii_case("regexp")
21346                                        || s.eq_ignore_ascii_case("rlike")))
21347                                || (s.eq_ignore_ascii_case("similar")
21348                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21349                        || (matches!(self.peek(), Token::Not)
21350                            && matches!(self.tokens.get(self.pos + 1),
21351                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21352                                    || (self.mysql_dialect
21353                                        && (s.eq_ignore_ascii_case("regexp")
21354                                            || s.eq_ignore_ascii_case("rlike")))
21355                                    || s.eq_ignore_ascii_case("similar")))) => {}
21356            _ => return Ok(None),
21357        }
21358        let mut expr = lhs.clone();
21359        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21360        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21361        if min_prec <= 4 {
21362            if matches!(self.peek(), Token::Is) {
21363                self.advance();
21364                let negated = if matches!(self.peek(), Token::Not) {
21365                    self.advance();
21366                    true
21367                } else {
21368                    false
21369                };
21370                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21371                // mailrs pg_dump.
21372                if matches!(self.peek(), Token::Distinct) {
21373                    self.advance();
21374                    if !matches!(self.peek(), Token::From) {
21375                        return Err(self.err(format!(
21376                            "expected FROM after IS{} DISTINCT, got {:?}",
21377                            if negated { " NOT" } else { "" },
21378                            self.peek()
21379                        )));
21380                    }
21381                    self.advance();
21382                    // Right-hand side: parse at the same precedence
21383                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21384                    // groups as `x IS DISTINCT FROM (a + b)`.
21385                    let rhs = self.parse_expr(5)?;
21386                    let op = if negated {
21387                        BinOp::IsNotDistinctFrom
21388                    } else {
21389                        BinOp::IsDistinctFrom
21390                    };
21391                    expr = Expr::Binary {
21392                        op,
21393                        lhs: Box::new(expr),
21394                        rhs: Box::new(rhs),
21395                    };
21396                    {
21397                        return Ok(Some(expr));
21398                    }
21399                }
21400                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21401                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21402                // Lowers onto pg_is_json(x, kind); NOT wraps the
21403                // call in a logical negation.
21404                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21405                if s.eq_ignore_ascii_case("json"))
21406                {
21407                    self.advance(); // JSON
21408                    let kind = match self.peek() {
21409                        Token::Ident(s) | Token::QuotedIdent(s)
21410                            if matches!(
21411                                s.to_ascii_lowercase().as_str(),
21412                                "value" | "object" | "array" | "scalar"
21413                            ) =>
21414                        {
21415                            let k = s.to_ascii_lowercase();
21416                            self.advance();
21417                            k
21418                        }
21419                        _ => "value".to_string(),
21420                    };
21421                    let call = Expr::FunctionCall {
21422                        name: "pg_is_json".to_string(),
21423                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21424                    };
21425                    expr = if negated {
21426                        Expr::Unary {
21427                            op: UnOp::Not,
21428                            expr: Box::new(call),
21429                        }
21430                    } else {
21431                        call
21432                    };
21433                    {
21434                        return Ok(Some(expr));
21435                    }
21436                }
21437                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21438                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21439                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21440                {
21441                    let form_kw = match self.peek() {
21442                        Token::Ident(s) | Token::QuotedIdent(s)
21443                            if matches!(
21444                                s.to_ascii_uppercase().as_str(),
21445                                "NFC" | "NFD" | "NFKC" | "NFKD"
21446                            ) && matches!(
21447                                self.tokens.get(self.pos + 1),
21448                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21449                                    if n.eq_ignore_ascii_case("normalized")
21450                            ) =>
21451                        {
21452                            Some(s.to_ascii_uppercase())
21453                        }
21454                        _ => None,
21455                    };
21456                    let bare_normalized = form_kw.is_none()
21457                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21458                        if s.eq_ignore_ascii_case("normalized"));
21459                    if form_kw.is_some() || bare_normalized {
21460                        if form_kw.is_some() {
21461                            self.advance(); // form keyword
21462                        }
21463                        self.advance(); // NORMALIZED
21464                        let mut args = alloc::vec![expr];
21465                        if let Some(f) = form_kw {
21466                            args.push(Expr::Literal(Literal::String(f)));
21467                        }
21468                        let call = Expr::FunctionCall {
21469                            name: "is_normalized".to_string(),
21470                            args,
21471                        };
21472                        expr = if negated {
21473                            Expr::Unary {
21474                                op: UnOp::Not,
21475                                expr: Box::new(call),
21476                            }
21477                        } else {
21478                            call
21479                        };
21480                        {
21481                            return Ok(Some(expr));
21482                        }
21483                    }
21484                }
21485                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21486                // three-valued boolean tests. IS TRUE/FALSE never
21487                // return NULL, so they lower to CASE forms whose
21488                // ELSE catches the NULL branch; IS UNKNOWN on a
21489                // boolean is exactly IS NULL.
21490                if matches!(self.peek(), Token::True | Token::False)
21491                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21492                {
21493                    let tok = self.advance();
21494                    let test = match tok {
21495                        Token::True => Some(true),
21496                        Token::False => Some(false),
21497                        _ => None, // UNKNOWN
21498                    };
21499                    // v7.39 (round 328, V45) — kept as what the user
21500                    // wrote. These used to be lowered here into `CASE` /
21501                    // `IS NULL`; the semantics were right but the AST no
21502                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21503                    // was echoed back as
21504                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21505                    expr = Expr::BoolTest {
21506                        expr: Box::new(expr),
21507                        value: test,
21508                        negated,
21509                    };
21510                    {
21511                        return Ok(Some(expr));
21512                    }
21513                }
21514                if !matches!(self.peek(), Token::Null) {
21515                    return Err(self.err(format!(
21516                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21517                    if negated { " NOT" } else { "" },
21518                    self.peek()
21519                )));
21520                }
21521                self.advance();
21522                expr = Expr::IsNull {
21523                    expr: Box::new(expr),
21524                    negated,
21525                };
21526                {
21527                    return Ok(Some(expr));
21528                }
21529            }
21530        }
21531        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21532        if min_prec <= 5 {
21533            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21534            // Look one token ahead so a stray `NOT` not followed by any of
21535            // these flows through to the early return below untouched.
21536            let negated = if matches!(self.peek(), Token::Not) {
21537                let next = self.tokens.get(self.pos + 1);
21538                matches!(next, Some(Token::Between | Token::In | Token::Like))
21539                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21540                    || (self.mysql_dialect
21541                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21542                    || s.eq_ignore_ascii_case("similar"))
21543            } else {
21544                false
21545            };
21546            if negated {
21547                self.advance();
21548            }
21549            if matches!(self.peek(), Token::Between) {
21550                expr = self.parse_between_tail(expr, negated)?;
21551                {
21552                    return Ok(Some(expr));
21553                }
21554            }
21555            if matches!(self.peek(), Token::In) {
21556                if self.suppress_in_tail && !negated {
21557                    // POSITION(sub IN str) — IN belongs to the
21558                    // enclosing function syntax; stop here.
21559                    {
21560                        return Ok(None);
21561                    }
21562                }
21563                expr = self.parse_in_tail(expr, negated)?;
21564                {
21565                    return Ok(Some(expr));
21566                }
21567            }
21568            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21569            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21570            // (the SQL→regex transform runs inside, in the backtracking-
21571            // friendly shape SPG's matcher needs).
21572            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21573                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21574            {
21575                self.advance(); // SIMILAR
21576                self.advance(); // TO
21577                let pattern = self.parse_expr(6)?;
21578                let mut args = alloc::vec![expr, pattern];
21579                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21580                    self.advance();
21581                    args.push(self.parse_expr(6)?);
21582                }
21583                let call = Expr::FunctionCall {
21584                    name: "__similar_to".to_string(),
21585                    args,
21586                };
21587                expr = maybe_not(call, negated);
21588                {
21589                    return Ok(Some(expr));
21590                }
21591            }
21592            if matches!(self.peek(), Token::Like) {
21593                self.advance();
21594                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21595                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21596                    expr = q;
21597                    {
21598                        return Ok(Some(expr));
21599                    }
21600                }
21601                // Pattern at the same precedence as other comparison RHSes —
21602                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21603                let mut pattern = self.parse_expr(6)?;
21604                // `ESCAPE 'c'` — rewrite a literal pattern to the
21605                // default backslash escape at parse time. Custom
21606                // escapes on non-literal patterns would need
21607                // matcher support; error honestly.
21608                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21609                    self.advance();
21610                    let esc = self.parse_expr(6)?;
21611                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21612                }
21613                expr = Expr::Like {
21614                    expr: Box::new(expr),
21615                    pattern: Box::new(pattern),
21616                    negated,
21617                    case_insensitive: false,
21618                };
21619                {
21620                    return Ok(Some(expr));
21621                }
21622            }
21623            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21624            // keyword reaches us as a plain identifier.
21625            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21626                self.advance();
21627                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21628                    expr = q;
21629                    {
21630                        return Ok(Some(expr));
21631                    }
21632                }
21633                let pattern = self.parse_expr(6)?;
21634                expr = Expr::Like {
21635                    expr: Box::new(expr),
21636                    pattern: Box::new(pattern),
21637                    negated,
21638                    case_insensitive: true,
21639                };
21640                {
21641                    return Ok(Some(expr));
21642                }
21643            }
21644            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21645            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21646            // matches case-insensitively under the default collation, so it
21647            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21648            // `~*` operator uses, wrapped in NOT when negated.
21649            if self.mysql_dialect
21650                && matches!(self.peek(), Token::Ident(s)
21651                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21652            {
21653                self.advance();
21654                let pattern = self.parse_expr(6)?;
21655                let call = Expr::FunctionCall {
21656                    name: String::from("regexp_like"),
21657                    args: alloc::vec![
21658                        expr,
21659                        pattern,
21660                        Expr::Literal(Literal::String(String::from("i"))),
21661                    ],
21662                };
21663                return Ok(Some(maybe_not(call, negated)));
21664            }
21665        }
21666        let _ = expr;
21667        Ok(None)
21668    }
21669
21670    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21671        let mut lhs = self.parse_unary()?;
21672        let mut chain_len = 0usize;
21673        loop {
21674            // OPERATOR([schema.]op) reduces to its underlying
21675            // operator token before the normal dispatch.
21676            let explicit = self.peek_explicit_operator();
21677            let dispatch = match &explicit {
21678                Some((_, tok)) => self.binop_here(tok),
21679                None => self.binop_here(self.peek()),
21680            };
21681            let Some((op, prec)) = dispatch else {
21682                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21683                // of the symbol family. `binop_here` answers None for them
21684                // because they lower onto function calls rather than a
21685                // BinOp, and the fallback below reads `self.peek()` — the
21686                // word OPERATOR, not the operator. `pg_dump` writes every
21687                // catalog predicate this way, so its first query failed
21688                // and no dump ran:
21689                //
21690                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21691                //
21692                // Collapsing the wrapper to the operator it names puts the
21693                // token where the fallback already looks.
21694                if let Some((next, op_tok)) = explicit {
21695                    self.tokens.splice(self.pos..next, [op_tok]);
21696                }
21697                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21698                    lhs = e;
21699                    chain_len += 1;
21700                    if chain_len > MAX_BINARY_CHAIN {
21701                        return Err(self.err(alloc::format!(
21702                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21703                        )));
21704                    }
21705                    continue;
21706                }
21707                break;
21708            };
21709            if prec < min_prec {
21710                break;
21711            }
21712            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21713            // iteratively but evaluates and drops recursively;
21714            // depth beyond the budget overflows worker stacks.
21715            chain_len += 1;
21716            if chain_len > MAX_BINARY_CHAIN {
21717                return Err(self.err(alloc::format!(
21718                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21719                )));
21720            }
21721            match explicit {
21722                Some((end_pos, _)) => self.pos = end_pos,
21723                None => {
21724                    self.advance();
21725                }
21726            }
21727            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21728            // ANY is a bare ident; ALL is a reserved Token. Both
21729            // require an immediate `(` to disambiguate from
21730            // identifier columns named `any` / `all`.
21731            let any_kind = match self.peek() {
21732                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21733                    Some(false)
21734                }
21735                Token::Ident(s) | Token::QuotedIdent(s)
21736                    if (s.eq_ignore_ascii_case("any")
21737                        || s.eq_ignore_ascii_case("some")
21738                        || s.eq_ignore_ascii_case("all"))
21739                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21740                {
21741                    Some(!s.eq_ignore_ascii_case("all"))
21742                }
21743                _ => None,
21744            };
21745            if let Some(is_any) = any_kind {
21746                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21747                continue;
21748            }
21749            let rhs = self.parse_expr(prec + 1)?;
21750            lhs = Expr::Binary {
21751                lhs: Box::new(lhs),
21752                op,
21753                rhs: Box::new(rhs),
21754            };
21755        }
21756        Ok(lhs)
21757    }
21758
21759    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21760    /// and the array form.
21761    ///
21762    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21763    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21764    /// this block's `Expr` temporaries and four `format!` sites slots in
21765    /// that frame on every level of `((((1))))`, which never reaches it.
21766    #[inline(never)]
21767    fn parse_any_all_rhs(
21768        &mut self,
21769        lhs: Expr,
21770        op: BinOp,
21771        is_any: bool,
21772    ) -> Result<Expr, ParseError> {
21773        self.advance(); // ident
21774        self.advance(); // (
21775        // `x op ANY (SELECT …)` — the quantified-subquery
21776        // form. `= ANY` is exactly IN; the other operators
21777        // lower onto EXISTS over the subquery as a derived
21778        // table, comparing against its single projection
21779        // aliased __v (x's columns resolve correlated).
21780        // ALL is the negated-EXISTS complement; a NULL
21781        // element makes PG return NULL where this lowering
21782        // returns true — the NOT NULL column case (the
21783        // practical one) is exact.
21784        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21785            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21786            // legal PG too (round-151 sibling). Out-of-line
21787            // (#[inline(never)] helper) — this sits on
21788            // parse_expr's recursive frame and the two-armed
21789            // SELECT temporary blew the nesting-budget stack.
21790            let mut sub = self.parse_any_all_select_body()?;
21791            if !matches!(self.peek(), Token::RParen) {
21792                return Err(self.err(alloc::format!(
21793                    "expected ')' after ANY/ALL subquery, got {:?}",
21794                    self.peek()
21795                )));
21796            }
21797            self.advance();
21798            if sub.items.len() != 1 {
21799                return Err(self.err(alloc::format!(
21800                    "ANY/ALL subquery must return one column, got {}",
21801                    sub.items.len()
21802                )));
21803            }
21804            if is_any && matches!(op, BinOp::Eq) {
21805                return Ok(Expr::InSubquery {
21806                    expr: Box::new(lhs),
21807                    subquery: Box::new(sub),
21808                    negated: false,
21809                });
21810            }
21811            // The engine's subquery resolvers materialise
21812            // the single-column result into an ARRAY the
21813            // existing AnyAll three-valued eval consumes.
21814            return Ok(Expr::AnyAll {
21815                expr: Box::new(lhs),
21816                op,
21817                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21818                is_any,
21819            });
21820        }
21821        let arr = self.parse_expr(0)?;
21822        if !matches!(self.peek(), Token::RParen) {
21823            return Err(self.err(alloc::format!(
21824                "expected ')' after ANY/ALL argument, got {:?}",
21825                self.peek()
21826            )));
21827        }
21828        self.advance();
21829        Ok(Expr::AnyAll {
21830            expr: Box::new(lhs),
21831            op,
21832            array: Box::new(arr),
21833            is_any,
21834        })
21835    }
21836
21837    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21838    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21839    #[inline(never)]
21840    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21841        self.advance();
21842        let e = self.parse_expr(9)?;
21843        Ok(build_center_call(e))
21844    }
21845
21846    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21847    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21848    /// unary minus.
21849    ///
21850    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21851    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21852    /// the Expr-sized local stays out of that frame.
21853    #[inline(never)]
21854    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21855        self.advance();
21856        let e = self.parse_expr(9)?;
21857        Ok(Expr::FunctionCall {
21858            name: alloc::string::String::from(name),
21859            args: alloc::vec![e],
21860        })
21861    }
21862
21863    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21864    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21865    #[inline(never)]
21866    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21867        self.advance();
21868        let e = self.parse_expr(9)?;
21869        Ok(Expr::FunctionCall {
21870            name: alloc::string::String::from(if vertical {
21871                "isvertical"
21872            } else {
21873                "ishorizontal"
21874            }),
21875            args: alloc::vec![e],
21876        })
21877    }
21878
21879    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21880    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21881    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21882    #[inline(never)]
21883    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21884        self.advance();
21885        let e = self.parse_expr(9)?;
21886        Ok(Expr::Cast {
21887            expr: Box::new(e),
21888            target: CastTarget::Named("binary".to_string()),
21889        })
21890    }
21891
21892    /// The prefix operators that share one shape: take the token, parse
21893    /// an operand at `prec`, wrap it.
21894    ///
21895    /// `#[inline(never)]`, and one function instead of five arms, for the
21896    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21897    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21898    /// debug build gives EVERY arm's locals a slot in the frame, whichever
21899    /// arm runs. `((((1))))` reaches none of these arms and was carrying
21900    /// five `Expr`-sized locals per level for them anyway.
21901    #[inline(never)]
21902    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21903        self.advance();
21904        let e = self.parse_expr(prec)?;
21905        Ok(Expr::Unary {
21906            op,
21907            expr: Box::new(e),
21908        })
21909    }
21910
21911    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21912    /// and separate from it because of the literal folding below and the
21913    /// `format!` temporaries that folding needs.
21914    #[inline(never)]
21915    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21916        self.advance();
21917        // v7.39 (round 549) — fold the sign into an integer literal that
21918        // only fits once it is negative.
21919        //
21920        // `9223372036854775808` is one past i64::MAX, so the lexer hands
21921        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21922        // folds the sign first, so `-9223372036854775808` is a bigint
21923        // there — and `-9223372036854775808 - 1` raises "bigint out of
21924        // range" where SPG quietly answered -9223372036854775809, a value
21925        // no bigint can hold. The arithmetic itself was already checked;
21926        // only the literal's type was wrong.
21927        if let Token::Numeric(lit) = self.peek()
21928            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21929        {
21930            self.advance();
21931            return Ok(Expr::Literal(Literal::Integer(folded)));
21932        }
21933        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21934        // `<->` slotted into 5 and arithmetic shifted up).
21935        let e = self.parse_expr(9)?;
21936        Ok(Expr::Unary {
21937            op: UnOp::Neg,
21938            expr: Box::new(e),
21939        })
21940    }
21941
21942    /// tsquery `!!` prefix negation, lowered to the catalog function.
21943    /// Binds like unary minus. Out-of-line for the frame reason on
21944    /// `parse_unary_op`.
21945    #[inline(never)]
21946    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21947        self.advance();
21948        let e = self.parse_expr(9)?;
21949        Ok(Expr::FunctionCall {
21950            name: String::from("tsquery_not"),
21951            args: alloc::vec![e],
21952        })
21953    }
21954
21955    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21956        match self.peek() {
21957            // NOT binds tighter than AND / XOR / OR but looser than
21958            // comparisons — its operand takes everything ≥ the comparison
21959            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21960            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21961            // was rung 3, behaviour-identical when 3 was unused; AND now
21962            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21963            Token::Not => self.parse_unary_op(UnOp::Not, 4),
21964            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21965            // The body is out-of-line: `parse_unary` is one of the three
21966            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21967            // inline arm here overflowed the native stack in
21968            // `nesting_budget_errors_cleanly` — the guard test caught it,
21969            // exactly as the eval-side cliff did in rounds 346 and 351.
21970            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21971                self.parse_binary_prefix()
21972            }
21973            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21974            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21975            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21976            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21977            Token::Minus => self.parse_prefix_minus(),
21978            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21979            // worked only because the lexer reads it as one signed literal;
21980            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21981            // PG18 and MariaDB take all of them. Binds like unary minus.
21982            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21983            // Bitwise NOT binds like unary minus.
21984            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21985            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21986            // "center of" operator; desugars to center(x). The whole arm
21987            // is out-of-line: parse_unary sits on the per-nesting-level
21988            // frame chain that MAX_NEST_DEPTH is tuned against, so no
21989            // Expr-sized local may live in this frame.
21990            Token::TsMatch => self.parse_prefix_center(),
21991            // v7.39 (round 508) — the prefix operators that are named
21992            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21993            // is length. Out-of-line for the same nesting-frame reason as
21994            // parse_prefix_center — parse_unary sits on the recursive cycle
21995            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21996            // live in this frame.
21997            Token::At => self.parse_prefix_call("abs"),
21998            Token::Hash => self.parse_prefix_call("npoints"),
21999            Token::AtMinusAt => self.parse_prefix_call("length"),
22000            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22001            // "is horizontal" (lseg / line); desugars to the existing
22002            // isvertical()/ishorizontal() functions. Out-of-line for the
22003            // same nesting-frame reason as parse_prefix_center.
22004            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22005            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22006            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22007            _ => self.parse_atom(),
22008        }
22009    }
22010
22011    /// Parse a parenthesised scalar subquery body after the caller has consumed
22012    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22013    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22014    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22015    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22016    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22017    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22018    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22019    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22020    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22021    /// tips the deep-nesting test into a stack overflow).
22022    #[inline(never)]
22023    fn array_subquery_ahead(&self) -> bool {
22024        if !matches!(self.peek(), Token::LParen) {
22025            return false;
22026        }
22027        matches!(
22028            self.tokens.get(self.pos + 1),
22029            Some(Token::Select | Token::Values)
22030        ) || matches!(
22031            self.tokens.get(self.pos + 1),
22032            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22033        )
22034    }
22035
22036    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22037    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22038    /// locals stay off parse_atom's recursive frame (round 105).
22039    #[inline(never)]
22040    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22041        self.advance(); // consume `[`
22042        let mut items: Vec<Expr> = Vec::new();
22043        if !matches!(self.peek(), Token::RBracket) {
22044            loop {
22045                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22046                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22047                if matches!(self.peek(), Token::LBracket) {
22048                    items.push(self.parse_array_bracket_body()?);
22049                } else {
22050                    items.push(self.parse_expr(0)?);
22051                }
22052                match self.peek() {
22053                    Token::Comma => {
22054                        self.advance();
22055                    }
22056                    Token::RBracket => break,
22057                    other => {
22058                        return Err(self.err(alloc::format!(
22059                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22060                        )));
22061                    }
22062                }
22063            }
22064        }
22065        self.advance(); // consume `]`
22066        Ok(Expr::Array(items))
22067    }
22068
22069    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22070    /// is already consumed; the current token is `(`. Desugars to a scalar
22071    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22072    /// the subquery's single-column rows in order — reusing the existing
22073    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22074    /// keeps the large `Statement` local off parse_atom's recursive frame.
22075    #[inline(never)]
22076    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22077        self.advance(); // consume `(`
22078        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22079            if w.eq_ignore_ascii_case("with"));
22080        let sub = if is_with {
22081            self.advance(); // WITH
22082            self.parse_with_cte_then_select()?
22083        } else {
22084            self.parse_select_stmt()?
22085        };
22086        if !matches!(self.peek(), Token::RParen) {
22087            return Err(self.err(alloc::format!(
22088                "expected ')' to close ARRAY(subquery), got {:?}",
22089                self.peek()
22090            )));
22091        }
22092        self.advance(); // consume `)`
22093        // Reuse the parser to build the array_agg wrapper from the subquery's
22094        // canonical text — avoids hand-constructing the derived-table AST.
22095        let wrapper = alloc::format!(
22096            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22097        );
22098        let stmt = parse_statement(&wrapper)
22099            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22100        let Statement::Select(sel) = stmt else {
22101            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22102        };
22103        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22104    }
22105
22106    #[inline(never)]
22107    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22108        let inner = if is_with {
22109            self.advance(); // WITH
22110            self.parse_with_cte_then_select()?
22111        } else {
22112            self.parse_select_stmt()?
22113        };
22114        match self.advance() {
22115            Token::RParen => {
22116                let Statement::Select(s) = inner else {
22117                    return Err(ParseError {
22118                        message: "scalar subquery body must be a SELECT".into(),
22119                        token_pos: self.consumed_pos(),
22120                    });
22121                };
22122                Ok(Expr::ScalarSubquery(Box::new(s)))
22123            }
22124            other => Err(ParseError {
22125                message: format!("expected ')' after scalar subquery, got {other:?}"),
22126                token_pos: self.consumed_pos(),
22127            }),
22128        }
22129    }
22130
22131    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22132    /// literals. The lexer splits them into an ident + string; recombine
22133    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22134    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22135    /// frame for the `body` / `bits` strings and their char loops (the
22136    /// round-367 frame cliff, M20).
22137    #[inline(never)]
22138    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22139        let is_hex = match self.peek() {
22140            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22141            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22142            _ => return None,
22143        };
22144        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22145            return None;
22146        }
22147        self.advance();
22148        let Token::String(body) = self.advance() else {
22149            unreachable!("guarded above");
22150        };
22151        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22152        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22153        // (hex pairs, even count required — MariaDB errors on an odd
22154        // count); `b'1010'` packs its bits big-endian, left-padded to a
22155        // byte. Lower both onto the bytea cast.
22156        if self.mysql_dialect {
22157            if is_hex {
22158                if body.len() % 2 == 1 {
22159                    return Some(Err(self.err(alloc::format!(
22160                        "invalid hex string literal X'{body}': odd digit count"
22161                    ))));
22162                }
22163                for c in body.chars() {
22164                    if !c.is_ascii_hexdigit() {
22165                        return Some(Err(
22166                            self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
22167                        ));
22168                    }
22169                }
22170                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22171            }
22172            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22173                return Some(Err(
22174                    self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
22175                ));
22176            }
22177            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22178        }
22179        let bits = if is_hex {
22180            let mut out = String::with_capacity(body.len() * 4);
22181            for c in body.chars() {
22182                let Some(d) = c.to_digit(16) else {
22183                    return Some(Err(self.err(alloc::format!(
22184                        "invalid hexadecimal digit {c:?} in X'…' bit string"
22185                    ))));
22186                };
22187                out.push_str(&alloc::format!("{d:04b}"));
22188            }
22189            out
22190        } else {
22191            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22192                return Some(Err(self.err(alloc::format!(
22193                    "invalid binary digit {bad:?} in B'…' bit string"
22194                ))));
22195            }
22196            body
22197        };
22198        // Route through the postfix-cast loop so a chained cast like
22199        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22200        // of erroring at the `::`.
22201        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22202        // literal keeps its exact length, while an explicit `::bit` cast is
22203        // bit(1) with pad/truncate semantics (PG).
22204        Some(self.finish_postfix_casts(Expr::Cast {
22205            expr: Box::new(Expr::Literal(Literal::String(bits))),
22206            target: CastTarget::Named("__bit_literal".to_string()),
22207        }))
22208    }
22209
22210    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22211        if let Some(res) = self.try_parse_bit_string_literal() {
22212            return res;
22213        }
22214        let tok_pos = self.pos;
22215        match self.advance() {
22216            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22217            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22218            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22219            // carrying the source mantissa + scale so no precision is lost. A
22220            // literal too wide for i128 falls back to double precision.
22221            // Out-of-line (#[inline(never)]) — this arm sits on the
22222            // parse_expr recursion chain; its expansion locals must not
22223            // widen the recursive frame (debug frame-cliff discipline).
22224            Token::Numeric(s) => match numeric_token_to_literal(s) {
22225                Ok(lit) => Ok(Expr::Literal(lit)),
22226                Err(msg) => Err(self.err(msg)),
22227            },
22228            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22229            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22230            // (the lexer only emits this token in the MySQL dialect). Lower
22231            // onto the existing bytea cast; out-of-line to keep this arm off
22232            // the parse recursion frame.
22233            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22234            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22235            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22236            Token::Null => Ok(Expr::Literal(Literal::Null)),
22237            // v6.1.1 — `$N` placeholder. The actual Value lookup
22238            // happens in the engine eval path against the prepared-
22239            // statement bind buffer.
22240            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22241            Token::LParen => {
22242                // v4.10: `(SELECT ...)` in expression position is a
22243                // scalar subquery; otherwise it's a parenthesised
22244                // expression. Peek for SELECT keyword to dispatch.
22245                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22246                // lexes as Ident("with") (not a reserved token). The subquery body
22247                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22248                // so its large `Statement` local stays out of parse_atom's stack
22249                // frame — parse_atom is on the recursive `((…))` cycle and the
22250                // nesting budget is tuned to its frame size).
22251                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22252                    if s.eq_ignore_ascii_case("with"));
22253                if matches!(self.peek(), Token::Select) || is_with {
22254                    self.parse_paren_scalar_subquery(is_with)
22255                } else {
22256                    let e = self.parse_expr(0)?;
22257                    // `(a, b, …)` — a row constructor. Valid only
22258                    // in front of a comparison operator or [NOT]
22259                    // IN; both expand at parse time (lexicographic
22260                    // comparison / OR'd row equalities).
22261                    if matches!(self.peek(), Token::Comma) {
22262                        let mut row = alloc::vec![e];
22263                        while matches!(self.peek(), Token::Comma) {
22264                            self.advance();
22265                            row.push(self.parse_expr(0)?);
22266                        }
22267                        if !matches!(self.peek(), Token::RParen) {
22268                            return Err(self.err(alloc::format!(
22269                                "expected ')' after row constructor, got {:?}",
22270                                self.peek()
22271                            )));
22272                        }
22273                        self.advance();
22274                        // A bare `(a, b, …)` row constructor can carry postfix
22275                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22276                        // early return here skips parse_atom's tail postfix
22277                        // pass, so fold casts in explicitly. For the
22278                        // comparison / predicate forms nothing postfix follows,
22279                        // so this is a no-op.
22280                        return self
22281                            .parse_row_comparison_tail(row)
22282                            .and_then(|e| self.finish_postfix_casts(e));
22283                    }
22284                    match self.advance() {
22285                        Token::RParen => Ok(e),
22286                        other => Err(ParseError {
22287                            message: format!("expected ')', got {other:?}"),
22288                            token_pos: self.consumed_pos(),
22289                        }),
22290                    }
22291                }
22292            }
22293            Token::LBracket => self.parse_vector_literal_body(),
22294            Token::Extract => self.parse_extract_atom(),
22295            Token::Interval => self.parse_interval_atom(),
22296            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22297            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22298            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22299            // expression position calling the PG `left(string, n)` /
22300            // `right(string, n)` function; rebuild the AST as a regular
22301            // function call so the engine's apply_function dispatch picks
22302            // it up. Delegated to a #[inline(never)] helper so its locals
22303            // don't bloat this recursive `parse_atom` frame (the nesting
22304            // budget in `enter_nested` is tuned to parse_atom's size).
22305            Token::Left if matches!(self.peek(), Token::LParen) => {
22306                self.parse_lr_string_function_call("left")
22307            }
22308            Token::Right if matches!(self.peek(), Token::LParen) => {
22309                self.parse_lr_string_function_call("right")
22310            }
22311            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22312            // token; we match on the bare ident. NOT is a token
22313            // (consumed in the comparison rung), but `EXISTS (...)`
22314            // at the top of an expression starts here.
22315            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22316                self.parse_exists_atom(false)
22317            }
22318            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22319            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22320            // CASE is a bare ident; we dispatch on lowercase match.
22321            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22322                self.parse_case_atom()
22323            }
22324            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22325            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22326            // '…'`. Lower onto the ::cast node so the existing
22327            // runtime text→date/timestamp paths do the parsing. The
22328            // string must follow immediately, else the ident stays a
22329            // plain column reference.
22330            Token::Ident(s)
22331                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22332                    && matches!(self.peek(), Token::String(_)) =>
22333            {
22334                let target =
22335                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22336                let Token::String(lit) = self.advance() else {
22337                    unreachable!("peek guaranteed a string token");
22338                };
22339                Ok(Expr::Cast {
22340                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22341                    target,
22342                })
22343            }
22344            // v7.39 (round 221) — the SQL-standard long spellings:
22345            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22346            // TIME ZONE '…'`. Consume the modifier and lower to the same
22347            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22348            Token::Ident(s)
22349                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22350                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22351                        || w.eq_ignore_ascii_case("without"))
22352                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22353                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22354                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22355            {
22356                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22357                self.advance(); // WITH / WITHOUT
22358                self.advance(); // TIME
22359                self.advance(); // ZONE
22360                let Token::String(lit) = self.advance() else {
22361                    unreachable!("guard checked a string token");
22362                };
22363                let base = s.to_ascii_lowercase();
22364                let target = match (base.as_str(), with_tz) {
22365                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22366                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22367                    (_, true) => CastTarget::Timestamptz,
22368                    (_, false) => CastTarget::Timestamp,
22369                };
22370                Ok(Expr::Cast {
22371                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22372                    target,
22373                })
22374            }
22375            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22376            // gathers the subquery's single-column rows (in its row order)
22377            // into an array. Desugared to `array_agg` over the subquery as a
22378            // derived table; out-of-line to keep parse_atom's frame small (it
22379            // sits on the recursive nesting-budget cycle).
22380            Token::Ident(s) | Token::QuotedIdent(s)
22381                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22382            {
22383                self.parse_array_subquery()
22384            }
22385            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22386            // is not a reserved token; we match by case-insensitive
22387            // ident. The opening `[` must follow immediately. v7.39 (read01
22388            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22389            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22390            Token::Ident(s) | Token::QuotedIdent(s)
22391                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22392            {
22393                self.parse_array_literal_body()
22394            }
22395            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22396            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22397            // We special-case before the generic ident dispatch so
22398            // the AGAINST clause never reaches the function-call
22399            // loop (which would mis-read `(cols) AGAINST` as a
22400            // call with no trailing modifier). The shape is
22401            // rewritten to a Boolean OR over per-column
22402            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22403            // term)` so the existing FTS evaluator handles
22404            // semantics — the fulltext-GIN built at CREATE TABLE
22405            // time is currently a "real index that survives dump
22406            // round-trip"; the planner hook that actually uses
22407            // it for posting-list intersection lands in a later
22408            // sub-phase (Phase 2.2b) without touching this surface.
22409            Token::Ident(s) | Token::QuotedIdent(s)
22410                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22411            {
22412                self.parse_match_against_atom()
22413            }
22414            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22415            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22416            // alias names in expression context too. `release` appears
22417            // in sentori `0003_partition_events.sql` as both a column
22418            // reference (SELECT … release …) and an INSERT column list
22419            // entry. Mirrors `expect_ident_like`'s expansion of the
22420            // identifier set.
22421            other if unreserved_keyword_text(&other).is_some() => {
22422                let s = unreserved_keyword_text(&other).unwrap();
22423                self.finish_ident_atom(s)
22424            }
22425            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22426            // only inside `SET` before, so `SELECT @@autocommit` — which
22427            // every MySQL connector asks at handshake — was a parse error.
22428            // MariaDB accepts the bare, `@@session.` and `@@global.`
22429            // spellings alike and answers from the session's own value.
22430            Token::SessionVar(v) => {
22431                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22432                // has nothing to do with a `@@` engine setting: its own
22433                // per-session namespace, and an unset one reads NULL instead
22434                // of raising. Stripping every `@` (as this did) made `@x` and
22435                // `@@x` the same node, so `SELECT @x` answered "Unknown
22436                // system variable".
22437                Ok(variable_ref_atom(&v))
22438            }
22439            other => Err(ParseError {
22440                message: format!("unexpected token {other:?} in expression"),
22441                token_pos: tok_pos,
22442            }),
22443        }
22444        // After parsing the atom, fold any postfix `::vector` casts.
22445        .and_then(|atom| self.finish_postfix_casts(atom))
22446    }
22447
22448    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22449    /// Both bind tighter than any binary op.
22450    /// Shared cast-target parser for postfix `::TYPE` and the
22451    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22452    /// If the next tokens are `( N )`, consume them and return the canonical
22453    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22454    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22455    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22456        if !matches!(self.peek(), Token::LParen) {
22457            return None;
22458        }
22459        self.advance(); // (
22460        let n = match self.advance() {
22461            Token::Integer(n) => n,
22462            _ => return Some(base.to_string()), // malformed → drop precision
22463        };
22464        if matches!(self.peek(), Token::RParen) {
22465            self.advance();
22466        }
22467        Some(alloc::format!("{base}({n})"))
22468    }
22469
22470    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22471        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22472        // schema-qualifies every cast target, and `pg_catalog.X` names
22473        // exactly the builtin type X. Consume the qualifier and let
22474        // the ordinary target parse decide.
22475        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22476            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22477        {
22478            self.advance();
22479            self.advance();
22480        }
22481        let target = match self.advance() {
22482            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22483                "int" | "integer" | "int4" => {
22484                    if matches!(self.peek(), Token::LBracket)
22485                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22486                    {
22487                        self.advance();
22488                        self.advance();
22489                        CastTarget::IntArray
22490                    } else {
22491                        CastTarget::Int
22492                    }
22493                }
22494                "bigint" | "int8" => {
22495                    if matches!(self.peek(), Token::LBracket)
22496                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22497                    {
22498                        self.advance();
22499                        self.advance();
22500                        CastTarget::BigIntArray
22501                    } else {
22502                        CastTarget::BigInt
22503                    }
22504                }
22505                "float" | "double" => CastTarget::Float,
22506                "text" => {
22507                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22508                    if matches!(self.peek(), Token::LBracket)
22509                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22510                    {
22511                        self.advance();
22512                        self.advance();
22513                        CastTarget::TextArray
22514                    } else {
22515                        CastTarget::Text
22516                    }
22517                }
22518                "bool" | "boolean" => CastTarget::Bool,
22519                "vector" => CastTarget::Vector,
22520                "date" => CastTarget::Date,
22521                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22522                // seconds precision through the Named path (the engine rounds
22523                // the sub-second field); bare `::timestamp` keeps the fast arm.
22524                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22525                    Some(named) => CastTarget::Named(named),
22526                    None => CastTarget::Timestamp,
22527                },
22528                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22529                    Some(named) => CastTarget::Named(named),
22530                    None => CastTarget::Timestamptz,
22531                },
22532                "interval" => CastTarget::Interval,
22533                "json" => CastTarget::Json,
22534                "jsonb" => CastTarget::Jsonb,
22535                // v7.39 (round 694) — these have dedicated CastTarget
22536                // variants, so they never reached the postfix `[]` handling
22537                // further down and `::regtype[]` was a SYNTAX error at the
22538                // `]`. PG has an array type for every scalar; take the
22539                // suffix here and hand the canonical `<ty>_array` name to
22540                // the engine, the same shape every other array cast uses.
22541                "regtype" if self.peek_postfix_array_brackets() => {
22542                    self.advance();
22543                    self.advance();
22544                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22545                }
22546                "regclass" if self.peek_postfix_array_brackets() => {
22547                    self.advance();
22548                    self.advance();
22549                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22550                }
22551                "regtype" => CastTarget::RegType,
22552                "regclass" => CastTarget::RegClass,
22553                // v7.12.0 — `::tsvector` / `::tsquery`.
22554                // Engine decodes the LHS text via the PG
22555                // external form parser.
22556                // v7.39 (round 352, M8) — MySQL's own cast targets.
22557                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22558                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22559                // such type, so they are taken only in that dialect and
22560                // fall through to the "type does not exist" arm otherwise.
22561                "signed" | "unsigned" if self.mysql_dialect => {
22562                    if matches!(self.peek(), Token::Ident(k)
22563                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22564                    {
22565                        self.advance();
22566                    }
22567                    CastTarget::Named(s.to_ascii_lowercase())
22568                }
22569                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22570                // in MySQL: MariaDB answers '123' where the SQL-standard
22571                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22572                // Truncating a number to its first digit is a wrong answer
22573                // with no error, so the MySQL session gets MySQL's reading.
22574                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22575                    CastTarget::Text
22576                }
22577                "tsvector" => CastTarget::TsVector,
22578                "tsquery" => CastTarget::TsQuery,
22579                // v7.17.0 — `::uuid`. Engine decodes the LHS
22580                // text via `spg_storage::parse_uuid_str`.
22581                "uuid" => CastTarget::Uuid,
22582                // v7.18 — `::bytea`. Engine decodes the LHS
22583                // text via the PG hex form (`'\xdeadbeef'`)
22584                // or escape form (`'\\x05\\x00'`). Closes
22585                // mailrs D-pre #3 reverse-acceptance gap.
22586                "bytea" => CastTarget::Bytea,
22587                // v7.37.5 ship triage — generic typed-cast escape.
22588                // Anything the long-tail PG type ident table knows
22589                // about(network/bit/geometry/multirange/etc.)flows
22590                // through `CastTarget::Named(canonical)`; the engine
22591                // resolves via `column_type_to_data_type` and dispatches
22592                // through the typed `coerce_value` path. Truly
22593                // unrecognised idents still hit the error arm below
22594                // because the engine rejects them.
22595                other => {
22596                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22597                    // `::varchar(255)`, etc. Capture into the canonical
22598                    // `name(p,s)` form so `type_name_to_data_type` can
22599                    // reconstruct the `DataType::Numeric { precision,
22600                    // scale }` (and similar param-carrying types).
22601                    let mut name = other.to_string();
22602                    // v7.39 (round 281) — `::bit varying(3)` is two
22603                    // words; fold the tail in so the typmod reaches the
22604                    // type resolver instead of tripping the parser.
22605                    if name.eq_ignore_ascii_case("bit")
22606                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22607                    {
22608                        self.advance();
22609                        name = alloc::string::String::from("varbit");
22610                    }
22611                    // v7.39 (round 613) — `::character varying` is the same
22612                    // two-word shape and had no fold, so the `varying` was
22613                    // left behind and the cast became a bare `character`,
22614                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22615                    // `a` where PG answers `ab`. Silently, and for a spelling
22616                    // pg_dump writes.
22617                    if name.eq_ignore_ascii_case("character")
22618                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22619                    {
22620                        self.advance();
22621                        name = alloc::string::String::from("varchar");
22622                    }
22623                    if matches!(self.peek(), Token::LParen) {
22624                        let mut buf = alloc::string::String::from("(");
22625                        let mut depth = 0usize;
22626                        loop {
22627                            match self.advance() {
22628                                Token::LParen => {
22629                                    depth += 1;
22630                                    if depth > 1 {
22631                                        buf.push('(');
22632                                    }
22633                                }
22634                                Token::RParen => {
22635                                    depth -= 1;
22636                                    if depth == 0 {
22637                                        buf.push(')');
22638                                        break;
22639                                    }
22640                                    buf.push(')');
22641                                }
22642                                Token::Comma => buf.push(','),
22643                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22644                                // v7.39 (round 273) — a minus used to fall
22645                                // into the catch-all below and vanish, so
22646                                // `::numeric(10,-2)` reached the engine as
22647                                // the text `numeric(10,2)` and silently
22648                                // rounded to two DECIMALS instead of to
22649                                // hundreds. A dropped token is not a
22650                                // no-op when it carries a sign.
22651                                Token::Minus => buf.push('-'),
22652                                Token::Eof => break,
22653                                _ => {}
22654                            }
22655                        }
22656                        name.push_str(&buf);
22657                    }
22658                    // Optional postfix `[]` widens to the array form —
22659                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22660                    // The engine's `type_name_to_data_type` recognises
22661                    // the canonical `<ty>_array` form.
22662                    if matches!(self.peek(), Token::LBracket)
22663                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22664                    {
22665                        self.advance();
22666                        self.advance();
22667                        name.push_str("_array");
22668                    }
22669                    CastTarget::Named(name)
22670                }
22671            },
22672            Token::Interval => CastTarget::Interval,
22673            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22674            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22675            // = char(1)); other quoted names resolve like idents.
22676            Token::QuotedIdent(q) => {
22677                if q.eq_ignore_ascii_case("char") {
22678                    CastTarget::Named("char1".into())
22679                } else {
22680                    CastTarget::Named(q.to_ascii_lowercase())
22681                }
22682            }
22683            other => {
22684                return Err(ParseError {
22685                    message: format!("expected type ident after `::`, got {other:?}"),
22686                    token_pos: self.consumed_pos(),
22687                });
22688            }
22689        };
22690        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22691        // target to its array sibling. Closed-enum arms (Bool /
22692        // SmallInt / Numeric / Float / Date / …) didn't carry the
22693        // explicit widening that Text / Int / BigInt did, so
22694        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22695        // error. The widening here mirrors the per-arm Text /
22696        // Int / BigInt logic above + folds the new ζ-A first-class
22697        // types through `CastTarget::Named("<ty>_array")`.
22698        if matches!(self.peek(), Token::LBracket)
22699            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22700        {
22701            let widened = match &target {
22702                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22703                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22704                // v7.39 (round 326, V43) — the two temporal types stay
22705                // distinct. Both used to widen to `timestamptz_array`, so
22706                // `::timestamp[]` named the wrong target in its own error
22707                // message and lost the zone-less identity on the way.
22708                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22709                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22710                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22711                CastTarget::Json | CastTarget::Jsonb => {
22712                    Some(CastTarget::Named("jsonb_array".to_string()))
22713                }
22714                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22715                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22716                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22717                CastTarget::Named(name) => {
22718                    let mut a = name.clone();
22719                    a.push_str("_array");
22720                    Some(CastTarget::Named(a))
22721                }
22722                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22723                // RegType / RegClass / TextArray / IntArray /
22724                // BigIntArray already finalised — leave as is.
22725                _ => None,
22726            };
22727            if let Some(w) = widened {
22728                self.advance();
22729                self.advance();
22730                return Ok(w);
22731            }
22732        }
22733        Ok(target)
22734    }
22735
22736    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22737        loop {
22738            // v7.38 (read01, T9) — composite field access `(expr).field`.
22739            // A bare `a.b` is consumed as a qualified column inside the ident
22740            // atom, so a Dot only survives to this postfix position when the
22741            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22742            // `.*` whole-row expansion is not handled here (projection-level).
22743            if matches!(self.peek(), Token::Dot)
22744                && matches!(
22745                    self.tokens.get(self.pos + 1),
22746                    Some(Token::Ident(_) | Token::QuotedIdent(_))
22747                )
22748            {
22749                self.advance(); // .
22750                let field = match self.advance() {
22751                    Token::Ident(s) | Token::QuotedIdent(s) => s,
22752                    other => {
22753                        return Err(
22754                            self.err(format!("expected a field name after '.', got {other:?}"))
22755                        );
22756                    }
22757                };
22758                expr = Expr::FieldAccess {
22759                    base: Box::new(expr),
22760                    field,
22761                };
22762                continue;
22763            }
22764            if matches!(self.peek(), Token::DoubleColon) {
22765                self.advance();
22766                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22767                // target set to include INTERVAL (reserved Token),
22768                // TIMESTAMPTZ, and PG catalog regtype / regclass.
22769                // mailrs follow-up H3a + H3b.
22770                let target = self.parse_cast_target()?;
22771                expr = Expr::Cast {
22772                    expr: Box::new(expr),
22773                    target,
22774                };
22775                continue;
22776            }
22777            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22778            // returns NULL for out-of-range. Multiple subscripts
22779            // chain: `a[i][j]` parses left-to-right.
22780            if matches!(self.peek(), Token::LBracket) {
22781                self.advance();
22782                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22783                // bare index stays a subscript.
22784                let lo = if matches!(self.peek(), Token::Colon) {
22785                    None
22786                } else {
22787                    Some(self.parse_expr(0)?)
22788                };
22789                if matches!(self.peek(), Token::Colon) {
22790                    self.advance();
22791                    let hi = if matches!(self.peek(), Token::RBracket) {
22792                        None
22793                    } else {
22794                        Some(Box::new(self.parse_expr(0)?))
22795                    };
22796                    if !matches!(self.peek(), Token::RBracket) {
22797                        return Err(self.err(alloc::format!(
22798                            "expected ']' after array slice, got {:?}",
22799                            self.peek()
22800                        )));
22801                    }
22802                    self.advance();
22803                    expr = Expr::ArraySlice {
22804                        target: Box::new(expr),
22805                        lo: lo.map(Box::new),
22806                        hi,
22807                    };
22808                    continue;
22809                }
22810                let index = lo.expect("non-colon branch parsed an index");
22811                if !matches!(self.peek(), Token::RBracket) {
22812                    return Err(self.err(alloc::format!(
22813                        "expected ']' after array index, got {:?}",
22814                        self.peek()
22815                    )));
22816                }
22817                self.advance();
22818                expr = Expr::ArraySubscript {
22819                    target: Box::new(expr),
22820                    index: Box::new(index),
22821                };
22822                continue;
22823            }
22824            // `expr AT TIME ZONE zone` — lowers to PG's own function
22825            // form timezone(zone, expr); the scalar implements the
22826            // offset shift (named zones error there — no tzdata).
22827            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22828                && matches!(self.tokens.get(self.pos + 1),
22829                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22830                && matches!(self.tokens.get(self.pos + 2),
22831                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22832            {
22833                self.advance(); // AT
22834                self.advance(); // TIME
22835                self.advance(); // ZONE
22836                // Zone at comparison precedence so AND/OR stay out.
22837                let zone = self.parse_expr(6)?;
22838                expr = Expr::FunctionCall {
22839                    name: "timezone".to_string(),
22840                    args: alloc::vec![zone, expr],
22841                };
22842                continue;
22843            }
22844            // `expr COLLATE "name"` — SPG's single text ordering IS
22845            // byte order, i.e. the C collation. The byte-order
22846            // spellings absorb as no-ops; a locale collation would
22847            // silently sort differently from PG, so it errors
22848            // honestly instead.
22849            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22850                self.advance();
22851                let mut cname = match self.advance() {
22852                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22853                    other => {
22854                        return Err(self.err(alloc::format!(
22855                            "expected collation name after COLLATE, got {other:?}"
22856                        )));
22857                    }
22858                };
22859                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22860                // is how `pg_dump` writes the default one:
22861                // `… COLLATE pg_catalog.default`. Reading a single token
22862                // left the SCHEMA as the name, so the clause was refused
22863                // as an unsupported locale collation and no dump ran.
22864                if matches!(self.peek(), Token::Dot) {
22865                    self.advance();
22866                    cname = match self.advance() {
22867                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22868                        // `default` lexes as a KEYWORD, and it is the name
22869                        // pg_dump writes — the same trap round 535 hit with
22870                        // TABLE / INDEX / FULL.
22871                        Token::Default => alloc::string::String::from("default"),
22872                        other => {
22873                            return Err(self.err(alloc::format!(
22874                                "expected collation name after COLLATE, got {other:?}"
22875                            )));
22876                        }
22877                    };
22878                }
22879                let lc = cname.to_ascii_lowercase();
22880                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22881                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22882                // family / `binary`) forces byte-wise, which is exactly
22883                // what `BINARY expr` does — lower onto that so every fold
22884                // site (comparison, LIKE, ORDER BY) suppresses via
22885                // `is_binary_coerced`. A `_ci` family override folds, and
22886                // under the MySQL dialect the default already folds, so it
22887                // absorbs as a no-op; likewise the C / byte-order spellings.
22888                if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22889                    expr = Expr::Cast {
22890                        expr: alloc::boxed::Box::new(expr),
22891                        target: CastTarget::Named("binary".to_string()),
22892                    };
22893                    continue;
22894                }
22895                let mysql_ci = self.mysql_dialect
22896                    && (lc.ends_with("_ci")
22897                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22898                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22899                // goes to the lowering channel, the byte-order spellings
22900                // included. Round 691 recorded only the names the old
22901                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22902                // absorbed as a no-op — and once a column could declare a
22903                // collation, absorbing the clause meant the COLUMN's
22904                // collation won where the query had asked for bytes.
22905                if self.in_order_by_key && !mysql_ci {
22906                    self.order_key_collation = Some(cname);
22907                    continue;
22908                }
22909                if !matches!(
22910                    lc.as_str(),
22911                    "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22912                ) && !mysql_ci
22913                {
22914                    // v7.38.18 — the old message read "SPG orders text
22915                    // by bytes (the C collation); locale collations are
22916                    // not supported yet", and both halves were false by
22917                    // the time it was read. This build performs locale
22918                    // collations: declared on a column or written in an
22919                    // ORDER BY key, `en_US.utf8` orders `apple, client,
22920                    // DateStyle, Zebra` exactly as PG 18.4 does. What it
22921                    // cannot do is carry a collation on an arbitrary
22922                    // expression, because there is no `Expr::Collate` to
22923                    // carry it — so say that, and say where the clause
22924                    // does work rather than telling the reader to drop it.
22925                    return Err(self.err(alloc::format!(
22926                        "COLLATE {cname:?} is not supported in this position: \
22927                         SPG carries a collation on a column declaration and \
22928                         on an ORDER BY key, not on an arbitrary expression. \
22929                         Declare it on the column (`x text COLLATE \
22930                         {cname:?}`) or move it into the ORDER BY key"
22931                    )));
22932                }
22933                continue;
22934            }
22935            return Ok(expr);
22936        }
22937    }
22938
22939    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22940    /// the first token that is not one. Schema qualifiers collapse to the
22941    /// last part, which is what every other name path here does (SPG is
22942    /// single-schema).
22943    fn take_comma_separated_names(&mut self) -> Vec<String> {
22944        let mut out = Vec::new();
22945        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22946            self.advance();
22947            let mut last = n;
22948            while matches!(self.peek(), Token::Dot) {
22949                self.advance();
22950                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22951                    last = t;
22952                }
22953            }
22954            out.push(last);
22955            if matches!(self.peek(), Token::Comma) {
22956                self.advance();
22957            } else {
22958                break;
22959            }
22960        }
22961        out
22962    }
22963
22964    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22965    ///
22966    /// The general cast-target path tests this inline; the types with their
22967    /// own `CastTarget` variant need it as a guard on their match arm,
22968    /// which is what this exists for.
22969    fn peek_postfix_array_brackets(&self) -> bool {
22970        matches!(self.peek(), Token::LBracket)
22971            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22972    }
22973
22974    /// Parse the operator tail after a `(a, b, …)` row constructor
22975    /// and expand at parse time. `=` is the conjunction of element
22976    /// equalities; `<>` its negation; the order operators expand
22977    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22978    /// equalities. Anything else (a bare row value, a subquery
22979    /// RHS) errors honestly — SPG has no composite runtime value.
22980    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22981        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22982            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22983                lhs: Box::new(l.clone()),
22984                op: BinOp::Eq,
22985                rhs: Box::new(r.clone()),
22986            });
22987            let first = it.next().expect("row has at least two elements");
22988            it.fold(first, |acc, e| Expr::Binary {
22989                lhs: Box::new(acc),
22990                op: BinOp::And,
22991                rhs: Box::new(e),
22992            })
22993        }
22994        // Lexicographic (a,b) OP (c,d):
22995        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
22996        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22997            if lhs.len() == 1 {
22998                return Expr::Binary {
22999                    lhs: Box::new(lhs[0].clone()),
23000                    op: last,
23001                    rhs: Box::new(rhs[0].clone()),
23002                };
23003            }
23004            let head_strict = Expr::Binary {
23005                lhs: Box::new(lhs[0].clone()),
23006                op: strict,
23007                rhs: Box::new(rhs[0].clone()),
23008            };
23009            let head_eq = Expr::Binary {
23010                lhs: Box::new(lhs[0].clone()),
23011                op: BinOp::Eq,
23012                rhs: Box::new(rhs[0].clone()),
23013            };
23014            Expr::Binary {
23015                lhs: Box::new(head_strict),
23016                op: BinOp::Or,
23017                rhs: Box::new(Expr::Binary {
23018                    lhs: Box::new(head_eq),
23019                    op: BinOp::And,
23020                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23021                }),
23022            }
23023        }
23024        let negated_in = if matches!(self.peek(), Token::Not)
23025            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23026        {
23027            self.advance();
23028            true
23029        } else {
23030            false
23031        };
23032        if matches!(self.peek(), Token::In) {
23033            self.advance();
23034            if !matches!(self.peek(), Token::LParen) {
23035                return Err(self.err(alloc::format!(
23036                    "expected '(' after row IN, got {:?}",
23037                    self.peek()
23038                )));
23039            }
23040            self.advance();
23041            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23042            // not a list of literal rows. Row-vs-list decomposes to
23043            // OR-of-AND above, but the subquery's rows are only known at
23044            // runtime, so keep it as a RowInSubquery node.
23045            if matches!(self.peek(), Token::Select) {
23046                let inner = self.parse_select_stmt()?;
23047                if !matches!(self.peek(), Token::RParen) {
23048                    return Err(self.err(alloc::format!(
23049                        "expected ')' after row IN-subquery, got {:?}",
23050                        self.peek()
23051                    )));
23052                }
23053                self.advance();
23054                let Statement::Select(s) = inner else {
23055                    unreachable!("parse_select_stmt always returns Statement::Select")
23056                };
23057                return Ok(Expr::RowInSubquery {
23058                    row,
23059                    subquery: Box::new(s),
23060                    negated: negated_in,
23061                });
23062            }
23063            let mut alternatives: Vec<Expr> = Vec::new();
23064            loop {
23065                // Optional ROW keyword before the paren row.
23066                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23067                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23068                {
23069                    self.advance();
23070                }
23071                if !matches!(self.peek(), Token::LParen) {
23072                    return Err(self.err(alloc::format!(
23073                        "expected '(' to open a row inside IN, got {:?}",
23074                        self.peek()
23075                    )));
23076                }
23077                self.advance();
23078                let mut rhs = alloc::vec![self.parse_expr(0)?];
23079                while matches!(self.peek(), Token::Comma) {
23080                    self.advance();
23081                    rhs.push(self.parse_expr(0)?);
23082                }
23083                if !matches!(self.peek(), Token::RParen) {
23084                    return Err(self.err(alloc::format!(
23085                        "expected ')' after row inside IN, got {:?}",
23086                        self.peek()
23087                    )));
23088                }
23089                self.advance();
23090                if rhs.len() != row.len() {
23091                    return Err(self.err(alloc::format!(
23092                        "row IN arity mismatch: left has {}, right has {}",
23093                        row.len(),
23094                        rhs.len()
23095                    )));
23096                }
23097                alternatives.push(row_eq(&row, &rhs));
23098                if matches!(self.peek(), Token::Comma) {
23099                    self.advance();
23100                    continue;
23101                }
23102                break;
23103            }
23104            if !matches!(self.peek(), Token::RParen) {
23105                return Err(self.err(alloc::format!(
23106                    "expected ')' to close row IN list, got {:?}",
23107                    self.peek()
23108                )));
23109            }
23110            self.advance();
23111            let mut it = alternatives.into_iter();
23112            let first = it.next().expect("IN list has at least one row");
23113            let combined = it.fold(first, |acc, e| Expr::Binary {
23114                lhs: Box::new(acc),
23115                op: BinOp::Or,
23116                rhs: Box::new(e),
23117            });
23118            return Ok(maybe_not(combined, negated_in));
23119        }
23120        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23121        // two periods share at least one time point. Each pair is
23122        // normalised with least/greatest (PG accepts the endpoints
23123        // in either order), then lowered to the standard
23124        // `start1 < end2 AND start2 < end1` form.
23125        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23126            if row.len() != 2 {
23127                return Err(self.err(alloc::format!(
23128                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23129                    row.len()
23130                )));
23131            }
23132            self.advance();
23133            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23134                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23135            {
23136                self.advance();
23137            }
23138            if !matches!(self.peek(), Token::LParen) {
23139                return Err(self.err(alloc::format!(
23140                    "expected '(' after OVERLAPS, got {:?}",
23141                    self.peek()
23142                )));
23143            }
23144            self.advance();
23145            let r0 = self.parse_expr(0)?;
23146            if !matches!(self.peek(), Token::Comma) {
23147                return Err(self.err(alloc::format!(
23148                    "OVERLAPS needs (start, end) on the right, got {:?}",
23149                    self.peek()
23150                )));
23151            }
23152            self.advance();
23153            let r1 = self.parse_expr(0)?;
23154            if !matches!(self.peek(), Token::RParen) {
23155                return Err(self.err(alloc::format!(
23156                    "expected ')' after OVERLAPS pair, got {:?}",
23157                    self.peek()
23158                )));
23159            }
23160            self.advance();
23161            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23162                name: String::from(name),
23163                args: alloc::vec![a.clone(), b.clone()],
23164            };
23165            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23166                lhs: Box::new(lhs),
23167                op: BinOp::Lt,
23168                rhs: Box::new(rhs),
23169            };
23170            return Ok(Expr::Binary {
23171                lhs: Box::new(lt(
23172                    pair_fn("least", &row[0], &row[1]),
23173                    pair_fn("greatest", &r0, &r1),
23174                )),
23175                op: BinOp::And,
23176                rhs: Box::new(lt(
23177                    pair_fn("least", &r0, &r1),
23178                    pair_fn("greatest", &row[0], &row[1]),
23179                )),
23180            });
23181        }
23182        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23183        // PG, `IS NULL` is true only when EVERY field is NULL, and
23184        // `IS NOT NULL` is true only when every field is non-NULL — the
23185        // latter is NOT the negation of the former (a mixed row is
23186        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23187        // which reproduces exactly that all-fields semantics.
23188        if matches!(self.peek(), Token::Is) {
23189            self.advance();
23190            let negated = if matches!(self.peek(), Token::Not) {
23191                self.advance();
23192                true
23193            } else {
23194                false
23195            };
23196            if !matches!(self.peek(), Token::Null) {
23197                return Err(self.err(alloc::format!(
23198                    "expected NULL after row IS [NOT], got {:?}",
23199                    self.peek()
23200                )));
23201            }
23202            self.advance();
23203            let mut it = row.iter().map(|e| Expr::IsNull {
23204                expr: Box::new(e.clone()),
23205                negated,
23206            });
23207            let first = it.next().expect("row has at least two elements");
23208            return Ok(it.fold(first, |acc, e| Expr::Binary {
23209                lhs: Box::new(acc),
23210                op: BinOp::And,
23211                rhs: Box::new(e),
23212            }));
23213        }
23214        let op = match self.peek() {
23215            Token::Eq => BinOp::Eq,
23216            Token::NotEq => BinOp::NotEq,
23217            Token::Lt => BinOp::Lt,
23218            Token::LtEq => BinOp::LtEq,
23219            Token::Gt => BinOp::Gt,
23220            Token::GtEq => BinOp::GtEq,
23221            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23222            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23223            // constructor value, identical to the `ROW(a, b, …)` keyword form:
23224            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23225            // (`::text`, `.field`) applies at the caller just as it does for the
23226            // ROW(...) node. All the comparison / predicate forms returned above.
23227            _ => {
23228                return Ok(Expr::FunctionCall {
23229                    name: String::from("row"),
23230                    args: row,
23231                });
23232            }
23233        };
23234        self.advance();
23235        // Optional ROW keyword before the paren row.
23236        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23237            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23238        {
23239            self.advance();
23240        }
23241        if !matches!(self.peek(), Token::LParen) {
23242            return Err(self.err(alloc::format!(
23243                "expected '(' to open the right-hand row, got {:?}",
23244                self.peek()
23245            )));
23246        }
23247        self.advance();
23248        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23249        // subquery. Kept as a node (the subquery's row is a runtime value);
23250        // the literal-RHS form below still decomposes at parse time.
23251        if matches!(self.peek(), Token::Select) {
23252            let inner = self.parse_select_stmt()?;
23253            if !matches!(self.peek(), Token::RParen) {
23254                return Err(self.err(alloc::format!(
23255                    "expected ')' after row comparison subquery, got {:?}",
23256                    self.peek()
23257                )));
23258            }
23259            self.advance();
23260            let Statement::Select(s) = inner else {
23261                unreachable!("parse_select_stmt always returns Statement::Select")
23262            };
23263            return Ok(Expr::RowCmpSubquery {
23264                row,
23265                op,
23266                subquery: Box::new(s),
23267            });
23268        }
23269        let mut rhs = alloc::vec![self.parse_expr(0)?];
23270        while matches!(self.peek(), Token::Comma) {
23271            self.advance();
23272            rhs.push(self.parse_expr(0)?);
23273        }
23274        if !matches!(self.peek(), Token::RParen) {
23275            return Err(self.err(alloc::format!(
23276                "expected ')' after right-hand row, got {:?}",
23277                self.peek()
23278            )));
23279        }
23280        self.advance();
23281        if rhs.len() != row.len() {
23282            // v7.39 (round 239) — PG's wording (42601).
23283            return Err(self.err("unequal number of entries in row expressions".to_string()));
23284        }
23285        Ok(match op {
23286            BinOp::Eq => row_eq(&row, &rhs),
23287            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23288            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23289            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23290            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23291            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23292            _ => unreachable!("op restricted above"),
23293        })
23294    }
23295
23296    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23297    /// escape character becomes the matcher's default backslash:
23298    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23299    /// → the char itself, and any pre-existing backslash escapes
23300    /// itself so it stays literal. Both operands must be string
23301    /// literals — a runtime pattern would need matcher support.
23302    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23303        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23304            (&pattern, &esc)
23305        else {
23306            return Err(
23307                "LIKE ... ESCAPE requires string-literal pattern and escape \
23308                 (runtime escape characters are not supported yet)"
23309                    .into(),
23310            );
23311        };
23312        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23313        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23314        // multi-character escape is an error.
23315        let esc_ch: Option<char> = {
23316            let mut ch_iter = e.chars();
23317            match (ch_iter.next(), ch_iter.next()) {
23318                (Some(c), None) => Some(c),
23319                (None, _) => None,
23320                (Some(_), Some(_)) => {
23321                    return Err(alloc::format!(
23322                        "ESCAPE must be a single character, got {e:?}"
23323                    ));
23324                }
23325            }
23326        };
23327        let mut out = String::with_capacity(p.len() + 4);
23328        let mut chars = p.chars();
23329        while let Some(c) = chars.next() {
23330            if Some(c) == esc_ch {
23331                match chars.next() {
23332                    // Escaped wildcard / escaped escape → keep the
23333                    // next char literal via backslash.
23334                    Some(next) => {
23335                        out.push('\\');
23336                        out.push(next);
23337                    }
23338                    None => {
23339                        return Err("LIKE pattern ends with the escape character".into());
23340                    }
23341                }
23342            } else if c == '\\' && esc_ch != Some('\\') {
23343                // A raw backslash is literal under a custom (or absent) escape
23344                // — escape it for the backslash-based matcher.
23345                out.push('\\');
23346                out.push('\\');
23347            } else {
23348                out.push(c);
23349            }
23350        }
23351        Ok(Expr::Literal(Literal::String(out)))
23352    }
23353
23354    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23355    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23356    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23357    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23358    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23359    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23360    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23361    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23362    /// array expression errors honestly rather than silently mismatching.
23363    fn try_like_any_all(
23364        &mut self,
23365        base: &Expr,
23366        negated: bool,
23367        case_insensitive: bool,
23368    ) -> Result<Option<Expr>, ParseError> {
23369        let is_any = match self.peek() {
23370            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23371            Token::Ident(s)
23372                if s.eq_ignore_ascii_case("any")
23373                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23374            {
23375                true
23376            }
23377            _ => return Ok(None),
23378        };
23379        self.advance(); // ANY / ALL
23380        self.advance(); // '('
23381        let arr = self.parse_expr(0)?;
23382        if !matches!(self.peek(), Token::RParen) {
23383            return Err(self.err(format!(
23384                "expected ')' after LIKE {} argument, got {:?}",
23385                if is_any { "ANY" } else { "ALL" },
23386                self.peek()
23387            )));
23388        }
23389        self.advance(); // ')'
23390        let Expr::Array(items) = arr else {
23391            return Err(self.err(
23392                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23393            ));
23394        };
23395        let mut clauses = items.into_iter().map(|p| Expr::Like {
23396            expr: Box::new(base.clone()),
23397            pattern: Box::new(p),
23398            negated,
23399            case_insensitive,
23400        });
23401        let Some(first) = clauses.next() else {
23402            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23403            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23404        };
23405        let op = if is_any { BinOp::Or } else { BinOp::And };
23406        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23407            lhs: Box::new(acc),
23408            op,
23409            rhs: Box::new(c),
23410        });
23411        Ok(Some(combined))
23412    }
23413
23414    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23415    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23416    /// `AND` is not swallowed.
23417    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23418        self.advance(); // BETWEEN
23419        // SYMMETRIC — the bounds may arrive in either order; both
23420        // orientations OR together. ASYMMETRIC is the default and
23421        // absorbs as noise.
23422        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23423        {
23424            self.advance();
23425            true
23426        } else {
23427            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23428                self.advance();
23429            }
23430            false
23431        };
23432        let low = self.parse_expr(6)?;
23433        if !matches!(self.peek(), Token::And) {
23434            return Err(self.err(format!(
23435                "expected AND after BETWEEN low bound, got {:?}",
23436                self.peek()
23437            )));
23438        }
23439        self.advance();
23440        let high = self.parse_expr(6)?;
23441        let target = Box::new(expr);
23442        let range = |lo: Expr, hi: Expr| Expr::Binary {
23443            lhs: Box::new(Expr::Binary {
23444                lhs: target.clone(),
23445                op: BinOp::GtEq,
23446                rhs: Box::new(lo),
23447            }),
23448            op: BinOp::And,
23449            rhs: Box::new(Expr::Binary {
23450                lhs: target.clone(),
23451                op: BinOp::LtEq,
23452                rhs: Box::new(hi),
23453            }),
23454        };
23455        let combined = if symmetric {
23456            Expr::Binary {
23457                lhs: Box::new(range(low.clone(), high.clone())),
23458                op: BinOp::Or,
23459                rhs: Box::new(range(high, low)),
23460            }
23461        } else {
23462            range(low, high)
23463        };
23464        Ok(maybe_not(combined, negated))
23465    }
23466
23467    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23468    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23469    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23470    /// Caller already consumed the leading `WITH` ident.
23471    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23472    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23473    /// self-reference that appears more than once in a single term.
23474    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23475        use crate::ast::{CteBody, SelectStatement};
23476        if !cte.recursive {
23477            return Ok(());
23478        }
23479        let CteBody::Select(body) = &cte.body else {
23480            return Ok(());
23481        };
23482        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23483        // check the anchor and every peer term.
23484        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23485        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23486        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23487            return Err(self.err(String::from(
23488                "ORDER BY in a recursive query is not implemented",
23489            )));
23490        }
23491        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23492            return Err(self.err(String::from(
23493                "LIMIT in a recursive query is not implemented",
23494            )));
23495        }
23496        let self_refs = |s: &SelectStatement| -> usize {
23497            let Some(from) = &s.from else {
23498                return 0;
23499            };
23500            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23501            for j in &from.joins {
23502                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23503                    n += 1;
23504                }
23505            }
23506            n
23507        };
23508        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23509            return Err(self.err(alloc::format!(
23510                "recursive reference to query \"{}\" must not appear more than once",
23511                cte.name
23512            )));
23513        }
23514        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23515        // apply only when the body actually references itself (a non-self-
23516        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23517        let anchor_refs = self_refs(body);
23518        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23519        if anchor_refs > 0 || union_refs {
23520            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23521            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23522            // "does not have the form" error — SPG used to compute a value.
23523            if body.unions.is_empty()
23524                || body.unions.iter().any(|(k, _)| {
23525                    !matches!(
23526                        k,
23527                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23528                    )
23529                })
23530            {
23531                return Err(self.err(alloc::format!(
23532                    "recursive query \"{}\" does not have the form non-recursive-term \
23533                     UNION [ALL] recursive-term",
23534                    cte.name
23535                )));
23536            }
23537            if anchor_refs > 0 {
23538                return Err(self.err(alloc::format!(
23539                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23540                    cte.name
23541                )));
23542            }
23543        }
23544        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23545        for (_, u) in &body.unions {
23546            if self_refs(u) == 0 {
23547                continue;
23548            }
23549            // The self-reference must not sit on the nullable side of an outer
23550            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23551            if let Some(from) = &u.from {
23552                for (i, j) in from.joins.iter().enumerate() {
23553                    let left_has_self = is_self(&from.primary)
23554                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23555                    let violated = match j.kind {
23556                        crate::ast::JoinKind::Left => is_self(&j.table),
23557                        crate::ast::JoinKind::Right => left_has_self,
23558                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23559                        _ => false,
23560                    };
23561                    if violated {
23562                        return Err(self.err(alloc::format!(
23563                            "recursive reference to query \"{}\" must not appear within an outer join",
23564                            cte.name
23565                        )));
23566                    }
23567                }
23568            }
23569            // No aggregates at the top level of the recursive term (SPG used
23570            // to run them and surface a misleading downstream error).
23571            let mut items_and_having: Vec<&Expr> = Vec::new();
23572            for it in &u.items {
23573                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23574                    items_and_having.push(expr);
23575                }
23576            }
23577            if let Some(h) = &u.having {
23578                items_and_having.push(h);
23579            }
23580            for e in items_and_having {
23581                if expr_has_toplevel_aggregate(e) {
23582                    return Err(self.err(String::from(
23583                        "aggregate functions are not allowed in a recursive query's recursive term",
23584                    )));
23585                }
23586            }
23587        }
23588        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23589        // subquery) anywhere in the body is rejected; a plain FROM derived
23590        // table is legal in PG and untouched here.
23591        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23592        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23593        for term in all_terms {
23594            if select_has_self_ref_in_sublink(term, &cte.name) {
23595                return Err(self.err(alloc::format!(
23596                    "recursive reference to query \"{}\" must not appear within a subquery",
23597                    cte.name
23598                )));
23599            }
23600        }
23601        Ok(())
23602    }
23603
23604    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23605    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23606    /// right after parse so the engine sees a plain recursive CTE with the
23607    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23608    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23609    /// text-rendered rows can't provide, and errors honestly.
23610    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23611        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23612        if cte.search.is_none() && cte.cycle.is_none() {
23613            return Ok(());
23614        }
23615        let cte_name = cte.name.clone();
23616        let col_names = cte.column_overrides.clone();
23617        if col_names.is_empty() {
23618            return Err(
23619                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23620            );
23621        }
23622        let search = cte.search.take();
23623        let cycle = cte.cycle.take();
23624        let mut extra_cols: Vec<String> = Vec::new();
23625        let col_ref = |name: &str| {
23626            Expr::Column(ColumnName {
23627                qualifier: Some(cte_name.clone()),
23628                name: name.to_string(),
23629            })
23630        };
23631        // Position of a SEARCH/CYCLE column within the CTE's column list.
23632        let pos_of = |name: &str| -> Result<usize, ParseError> {
23633            col_names
23634                .iter()
23635                .position(|c| c.eq_ignore_ascii_case(name))
23636                .ok_or_else(|| {
23637                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23638                })
23639        };
23640        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23641            let mut args = Vec::with_capacity(positions.len());
23642            for &p in positions {
23643                match items.get(p) {
23644                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23645                    _ => {
23646                        return Err(self.err(
23647                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23648                        ));
23649                    }
23650                }
23651            }
23652            Ok(Expr::FunctionCall {
23653                name: "row".into(),
23654                args,
23655            })
23656        };
23657        let CteBody::Select(body) = &mut cte.body else {
23658            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23659        };
23660        if body.unions.is_empty() {
23661            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23662        }
23663        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23664
23665        if let Some(srch) = search {
23666            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23667            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23668            // no typed `record[]`, but element-wise array ORDER BY is correct
23669            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23670            // exactly onto a typed array: DEPTH is the root→node path
23671            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23672            // orders numerically (multi-digit keys included), matching PG.
23673            //
23674            // A multi-column BY would need a record[] to keep the per-node key
23675            // tuple orderable, which SPG can't express — error honestly there
23676            // rather than mis-order.
23677            if srch.by_columns.len() != 1 {
23678                return Err(self.err(
23679                    "SEARCH … BY with multiple columns needs typed record[] ordering \
23680                     SPG doesn't have yet; a single BY column is supported"
23681                        .into(),
23682                ));
23683            }
23684            let key_pos = pos_of(&srch.by_columns[0])?;
23685            let base_key = match body.items.get(key_pos) {
23686                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23687                _ => {
23688                    return Err(
23689                        self.err("SEARCH BY column maps to a non-expression select item".into())
23690                    );
23691                }
23692            };
23693            let rec_key = match body.unions[rec].1.items.get(key_pos) {
23694                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23695                _ => {
23696                    return Err(
23697                        self.err("SEARCH BY column maps to a non-expression select item".into())
23698                    );
23699                }
23700            };
23701            if srch.depth_first {
23702                // base: ARRAY[key]; rec: array_append(cte.set, key).
23703                body.items.push(SelectItem::Expr {
23704                    expr: Expr::Array(alloc::vec![base_key]),
23705                    alias: Some(srch.set_column.clone()),
23706                });
23707                body.unions[rec].1.items.push(SelectItem::Expr {
23708                    expr: Expr::FunctionCall {
23709                        name: "array_append".into(),
23710                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
23711                    },
23712                    alias: Some(srch.set_column.clone()),
23713                });
23714            } else {
23715                // BREADTH: [depth, key]; depth starts at 0 and increments. The
23716                // leading depth element dominates the element-wise comparison,
23717                // so shallower rows sort first, then by key — PG's (depth, key).
23718                body.items.push(SelectItem::Expr {
23719                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23720                    alias: Some(srch.set_column.clone()),
23721                });
23722                // rec depth = cte.set[1] + 1.
23723                let parent_depth = Expr::ArraySubscript {
23724                    target: Box::new(col_ref(&srch.set_column)),
23725                    index: Box::new(Expr::Literal(Literal::Integer(1))),
23726                };
23727                body.unions[rec].1.items.push(SelectItem::Expr {
23728                    expr: Expr::Array(alloc::vec![
23729                        Expr::Binary {
23730                            lhs: Box::new(parent_depth),
23731                            op: BinOp::Add,
23732                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23733                        },
23734                        rec_key,
23735                    ]),
23736                    alias: Some(srch.set_column.clone()),
23737                });
23738            }
23739            extra_cols.push(srch.set_column);
23740        }
23741
23742        if let Some(cyc) = cycle {
23743            let positions: Vec<usize> = cyc
23744                .columns
23745                .iter()
23746                .map(|c| pos_of(c))
23747                .collect::<Result<_, _>>()?;
23748            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23749            // cast it to text for the cycle path: membership only needs equality,
23750            // and the record text form gives SPG a TextArray path (SPG has no
23751            // typed record[] array). Cycle detection is unaffected.
23752            let base_row = Expr::Cast {
23753                expr: Box::new(row_of(&body.items, &positions)?),
23754                target: CastTarget::Text,
23755            };
23756            let rec_row = Expr::Cast {
23757                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23758                target: CastTarget::Text,
23759            };
23760            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23761            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23762            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23763            body.items.push(SelectItem::Expr {
23764                expr: Expr::Literal(dflt.clone()),
23765                alias: Some(cyc.mark_column.clone()),
23766            });
23767            body.items.push(SelectItem::Expr {
23768                expr: Expr::Array(alloc::vec![base_row]),
23769                alias: Some(cyc.path_column.clone()),
23770            });
23771            // rec mark: ROW(cols) already in the path → cycle.
23772            let hit = Expr::AnyAll {
23773                expr: Box::new(rec_row.clone()),
23774                op: BinOp::Eq,
23775                array: Box::new(col_ref(&cyc.path_column)),
23776                is_any: true,
23777            };
23778            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23779                Expr::Case {
23780                    operand: None,
23781                    branches: alloc::vec![(hit, Expr::Literal(mark))],
23782                    else_branch: Some(Box::new(Expr::Literal(dflt))),
23783                }
23784            } else {
23785                hit
23786            };
23787            body.unions[rec].1.items.push(SelectItem::Expr {
23788                expr: mark_expr,
23789                alias: Some(cyc.mark_column.clone()),
23790            });
23791            // rec path: array_append(cte.path, ROW(cols)).
23792            body.unions[rec].1.items.push(SelectItem::Expr {
23793                expr: Expr::FunctionCall {
23794                    name: "array_append".into(),
23795                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23796                },
23797                alias: Some(cyc.path_column.clone()),
23798            });
23799            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23800            let stop = Expr::Unary {
23801                op: UnOp::Not,
23802                expr: Box::new(col_ref(&cyc.mark_column)),
23803            };
23804            let w = &mut body.unions[rec].1.where_;
23805            *w = Some(match w.take() {
23806                Some(prev) => Expr::Binary {
23807                    lhs: Box::new(prev),
23808                    op: BinOp::And,
23809                    rhs: Box::new(stop),
23810                },
23811                None => stop,
23812            });
23813            extra_cols.push(cyc.mark_column);
23814            extra_cols.push(cyc.path_column);
23815        }
23816        cte.column_overrides.extend(extra_cols);
23817        Ok(())
23818    }
23819
23820    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23821    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23822    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23823        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23824            return Ok(None);
23825        }
23826        self.advance(); // SEARCH
23827        let depth_first = match self.peek() {
23828            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23829            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23830            other => {
23831                return Err(self.err(format!(
23832                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23833                )));
23834            }
23835        };
23836        self.advance();
23837        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23838            return Err(self.err(format!(
23839                "expected FIRST after SEARCH mode, got {:?}",
23840                self.peek()
23841            )));
23842        }
23843        self.advance();
23844        if !self.peek_is_by() {
23845            return Err(self.err(format!(
23846                "expected BY after SEARCH … FIRST, got {:?}",
23847                self.peek()
23848            )));
23849        }
23850        self.advance();
23851        let mut by_columns = alloc::vec![self.expect_ident_like()?];
23852        while matches!(self.peek(), Token::Comma) {
23853            self.advance();
23854            by_columns.push(self.expect_ident_like()?);
23855        }
23856        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23857            return Err(self.err(format!(
23858                "expected SET in SEARCH clause, got {:?}",
23859                self.peek()
23860            )));
23861        }
23862        self.advance();
23863        let set_column = self.expect_ident_like()?;
23864        Ok(Some(crate::ast::SearchClause {
23865            depth_first,
23866            by_columns,
23867            set_column,
23868        }))
23869    }
23870
23871    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23872    /// USING pathcol`. Returns None when the next token isn't CYCLE.
23873    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23874        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23875            return Ok(None);
23876        }
23877        self.advance(); // CYCLE
23878        let mut columns = alloc::vec![self.expect_ident_like()?];
23879        while matches!(self.peek(), Token::Comma) {
23880            self.advance();
23881            columns.push(self.expect_ident_like()?);
23882        }
23883        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23884            return Err(self.err(format!(
23885                "expected SET in CYCLE clause, got {:?}",
23886                self.peek()
23887            )));
23888        }
23889        self.advance();
23890        let mark_column = self.expect_ident_like()?;
23891        let mut mark_value = None;
23892        let mut default_value = None;
23893        if matches!(self.peek(), Token::To) {
23894            self.advance();
23895            mark_value = Some(self.parse_cycle_literal()?);
23896            if !matches!(self.peek(), Token::Default) {
23897                return Err(self.err(format!(
23898                    "expected DEFAULT after CYCLE … TO, got {:?}",
23899                    self.peek()
23900                )));
23901            }
23902            self.advance();
23903            default_value = Some(self.parse_cycle_literal()?);
23904        }
23905        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23906            return Err(self.err(format!(
23907                "expected USING in CYCLE clause, got {:?}",
23908                self.peek()
23909            )));
23910        }
23911        self.advance();
23912        let path_column = self.expect_ident_like()?;
23913        Ok(Some(crate::ast::CycleClause {
23914            columns,
23915            mark_column,
23916            mark_value,
23917            default_value,
23918            path_column,
23919        }))
23920    }
23921
23922    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23923    /// literal (string / bool / number) in PG.
23924    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23925        match self.parse_expr(0)? {
23926            Expr::Literal(l) => Ok(l),
23927            other => Err(self.err(format!(
23928                "CYCLE mark/default value must be a literal, got {other:?}"
23929            ))),
23930        }
23931    }
23932
23933    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23934        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23935        // Comes through as an identifier; consume it if present and
23936        // mark every CTE in the clause as recursive (PG semantics —
23937        // the flag is per-WITH, not per-CTE).
23938        let mut recursive = false;
23939        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23940            && s.eq_ignore_ascii_case("recursive")
23941        {
23942            self.advance();
23943            recursive = true;
23944        }
23945        let mut ctes = Vec::new();
23946        loop {
23947            let name = self.expect_ident_like()?;
23948            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23949            // PG uses these to rename the body's output columns; we
23950            // do the same below by overriding `columns[i].name`.
23951            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23952                self.advance();
23953                let mut names = Vec::new();
23954                loop {
23955                    names.push(self.expect_ident_like()?);
23956                    if matches!(self.peek(), Token::Comma) {
23957                        self.advance();
23958                        continue;
23959                    }
23960                    break;
23961                }
23962                if !matches!(self.peek(), Token::RParen) {
23963                    return Err(self.err(format!(
23964                        "expected ')' to close CTE column list, got {:?}",
23965                        self.peek()
23966                    )));
23967                }
23968                self.advance();
23969                names
23970            } else {
23971                Vec::new()
23972            };
23973            // AS is a reserved Token::As (used by SELECT-item / FROM
23974            // aliasing) — handle it specially rather than as a bare
23975            // ident.
23976            if !matches!(self.peek(), Token::As) {
23977                return Err(self.err(format!(
23978                    "expected AS after CTE name {name:?}, got {:?}",
23979                    self.peek()
23980                )));
23981            }
23982            self.advance();
23983            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23984            // MATERIALIZED` optimizer hints. SPG materialises every
23985            // CTE, so both spellings are accepted and absorbed.
23986            if matches!(self.peek(), Token::Not) {
23987                self.advance(); // NOT
23988                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23989                    if s.eq_ignore_ascii_case("materialized"))
23990                {
23991                    self.advance();
23992                } else {
23993                    return Err(self.err(format!(
23994                        "expected MATERIALIZED after AS NOT, got {:?}",
23995                        self.peek()
23996                    )));
23997                }
23998            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23999                if s.eq_ignore_ascii_case("materialized"))
24000            {
24001                self.advance();
24002            }
24003            if !matches!(self.peek(), Token::LParen) {
24004                return Err(self.err(format!(
24005                    "expected '(' after AS in WITH clause, got {:?}",
24006                    self.peek()
24007                )));
24008            }
24009            self.advance();
24010            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24011            // RETURNING) as the CTE body in addition to SELECT.
24012            // PG writable CTE semantics. UPDATE / DELETE come in as
24013            // bare Idents (lexer keeps SELECT / INSERT as reserved
24014            // tokens but treats the rest of DML as case-insensitive
24015            // idents).
24016            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24017            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24018            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24019            let body = match self.peek() {
24020                Token::Select => {
24021                    let inner = self.parse_select_stmt()?;
24022                    let Statement::Select(s) = inner else {
24023                        unreachable!("parse_select_stmt returns Select");
24024                    };
24025                    crate::ast::CteBody::Select(s)
24026                }
24027                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24028                // `SELECT * FROM t` this way and accepts it wherever a
24029                // SELECT goes, so the CTE body dispatch needs its own
24030                // arm: this match is keyed on the FIRST token, and
24031                // `Token::Table` fell through to a tail that then
24032                // rejected what it got. `parse_table_shorthand` has
24033                // returned a desugared SelectStatement since the
24034                // shorthand landed — only the routing was missing.
24035                // Round 868 found this by putting the shorthand in a
24036                // subquery; every earlier check used a top-level form.
24037                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24038                // `SELECT * FROM t` this way and accepts it wherever a
24039                // SELECT goes, so the CTE body dispatch needs its own
24040                // arm: this match is keyed on the FIRST token, and
24041                // `Token::Table` fell through to a tail that rejected
24042                // what it got. `parse_table_shorthand` has returned a
24043                // desugared SelectStatement since the shorthand landed —
24044                // only the routing was missing, here and in the derived
24045                // table's second-token gate. Round 868 found both by
24046                // putting the shorthand in a subquery; every earlier
24047                // check had used a top-level form.
24048                Token::Table
24049                    if matches!(
24050                        self.tokens.get(self.pos + 1),
24051                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24052                    ) =>
24053                {
24054                    let mut head = self.parse_table_shorthand()?;
24055                    self.parse_setop_chain_into(&mut head)?;
24056                    self.parse_select_tail_into(&mut head)?;
24057                    crate::ast::CteBody::Select(head)
24058                }
24059                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24060                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24061                // the shared rows helper onto a Select body.
24062                Token::Values => {
24063                    self.advance(); // VALUES
24064                    let mut head = self.parse_values_rows_body()?;
24065                    // A VALUES seed can head a set-operation chain —
24066                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24067                    // SELECT n+1 FROM t …). Attach any trailing
24068                    // UNION / INTERSECT / EXCEPT peers so the
24069                    // recursive-CTE body parses like the SELECT seed.
24070                    self.parse_setop_chain_into(&mut head)?;
24071                    crate::ast::CteBody::Select(head)
24072                }
24073                Token::Insert => {
24074                    let inner = self.parse_one_statement()?;
24075                    let Statement::Insert(s) = inner else {
24076                        unreachable!("Token::Insert routes to Insert");
24077                    };
24078                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24079                }
24080                _ if is_update_kw => {
24081                    let inner = self.parse_one_statement()?;
24082                    let Statement::Update(s) = inner else {
24083                        return Err(
24084                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24085                        );
24086                    };
24087                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24088                }
24089                _ if is_delete_kw => {
24090                    let inner = self.parse_one_statement()?;
24091                    let Statement::Delete(s) = inner else {
24092                        return Err(
24093                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24094                        );
24095                    };
24096                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24097                }
24098                // v7.39 (round 149) — PG 17 allows MERGE as a
24099                // data-modifying CTE body.
24100                _ if is_merge_kw => {
24101                    let inner = self.parse_one_statement()?;
24102                    let Statement::Merge(s) = inner else {
24103                        return Err(
24104                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24105                        );
24106                    };
24107                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24108                }
24109                // v7.39 (round 151) — a CTE body may itself be
24110                // WITH-headed (PG grammar: PreparableStmt carries its
24111                // own with_clause). The nested statement keeps its own
24112                // ctes; the modifying-CTE-at-top-level rule is enforced
24113                // at execution.
24114                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24115                    self.advance(); // WITH
24116                    match self.parse_with_cte_then_select()? {
24117                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24118                        Statement::Insert(s) => {
24119                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24120                        }
24121                        Statement::Update(s) => {
24122                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24123                        }
24124                        Statement::Delete(s) => {
24125                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24126                        }
24127                        Statement::Merge(s) => {
24128                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24129                        }
24130
24131                        other => {
24132                            return Err(self.err(format!(
24133                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24134                            )));
24135                        }
24136                    }
24137                }
24138                other => {
24139                    return Err(self.err(format!(
24140                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24141                    )));
24142                }
24143            };
24144            if !matches!(self.peek(), Token::RParen) {
24145                return Err(self.err(format!(
24146                    "expected ')' after CTE body, got {:?}",
24147                    self.peek()
24148                )));
24149            }
24150            self.advance();
24151            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24152            // CTE, desugared into extra body columns by the engine.
24153            let search = self.parse_cte_search_clause()?;
24154            let cycle = self.parse_cte_cycle_clause()?;
24155            let mut cte = crate::ast::Cte {
24156                name,
24157                body,
24158                recursive,
24159                column_overrides,
24160                search,
24161                cycle,
24162            };
24163            self.validate_recursive_cte(&cte)?;
24164            self.desugar_cte_search_cycle(&mut cte)?;
24165            ctes.push(cte);
24166            if matches!(self.peek(), Token::Comma) {
24167                self.advance();
24168                continue;
24169            }
24170            break;
24171        }
24172        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24173        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24174        // the parsed CTEs to whichever statement the body produces.
24175        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24176        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24177        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24178        match self.peek() {
24179            Token::Select => {
24180                let body_stmt = self.parse_select_stmt()?;
24181                let Statement::Select(mut body) = body_stmt else {
24182                    unreachable!()
24183                };
24184                body.ctes = ctes;
24185                Ok(Statement::Select(body))
24186            }
24187            Token::Insert => {
24188                let body_stmt = self.parse_one_statement()?;
24189                let Statement::Insert(mut body) = body_stmt else {
24190                    unreachable!()
24191                };
24192                body.ctes = ctes;
24193                Ok(Statement::Insert(body))
24194            }
24195            _ if outer_is_update => {
24196                let body_stmt = self.parse_one_statement()?;
24197                let Statement::Update(mut body) = body_stmt else {
24198                    return Err(self.err(format!("expected UPDATE after WITH clause")));
24199                };
24200                body.ctes = ctes;
24201                Ok(Statement::Update(body))
24202            }
24203            _ if outer_is_delete => {
24204                let body_stmt = self.parse_one_statement()?;
24205                let Statement::Delete(mut body) = body_stmt else {
24206                    return Err(self.err(format!("expected DELETE after WITH clause")));
24207                };
24208                body.ctes = ctes;
24209                Ok(Statement::Delete(body))
24210            }
24211            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24212            // WITH RECURSIVE is rejected with PG's exact message
24213            // (parse analysis, transformWithClause).
24214            _ if outer_is_merge => {
24215                if recursive {
24216                    return Err(self.err(String::from(
24217                        "WITH RECURSIVE is not supported for MERGE statement",
24218                    )));
24219                }
24220                let body_stmt = self.parse_one_statement()?;
24221                let Statement::Merge(mut body) = body_stmt else {
24222                    return Err(self.err(format!("expected MERGE after WITH clause")));
24223                };
24224                body.ctes = ctes;
24225                Ok(Statement::Merge(body))
24226            }
24227            other => Err(self.err(format!(
24228                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24229            ))),
24230        }
24231    }
24232
24233    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24234    /// already consumed the leading `EXISTS` ident via
24235    /// `self.advance()`.
24236    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24237    /// the leading `CASE` ident has been consumed (mailrs round-5
24238    /// G9). Supports both the searched form
24239    /// (`CASE WHEN cond THEN val …`) and the simple form
24240    /// (`CASE operand WHEN val THEN val …`).
24241    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24242        // Disambiguate searched vs simple form: if the next token
24243        // is `WHEN`, we're in the searched form. Otherwise the
24244        // intervening expression is the operand.
24245        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24246            None
24247        } else {
24248            Some(Box::new(self.parse_expr(0)?))
24249        };
24250        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24251        loop {
24252            match self.peek() {
24253                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24254                    self.advance();
24255                    let cond = self.parse_expr(0)?;
24256                    match self.peek() {
24257                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24258                            self.advance();
24259                        }
24260                        other => {
24261                            return Err(self.err(alloc::format!(
24262                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24263                            )));
24264                        }
24265                    }
24266                    let value = self.parse_expr(0)?;
24267                    branches.push((cond, value));
24268                }
24269                _ => break,
24270            }
24271        }
24272        if branches.is_empty() {
24273            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24274        }
24275        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24276        {
24277            self.advance();
24278            Some(Box::new(self.parse_expr(0)?))
24279        } else {
24280            None
24281        };
24282        match self.peek() {
24283            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24284                self.advance();
24285            }
24286            other => {
24287                return Err(self.err(alloc::format!(
24288                    "expected END to close CASE expression, got {other:?}"
24289                )));
24290            }
24291        }
24292        Ok(Expr::Case {
24293            operand,
24294            branches,
24295            else_branch,
24296        })
24297    }
24298
24299    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24300    /// query-source position (EXISTS / IN / INSERT source / CTE body /
24301    /// view body). Caller consumed the WITH keyword. Only a SELECT
24302    /// outer is grammatical here; the data-modifying-CTE-at-top-level
24303    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24304    /// maps correctly.
24305    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24306        let inner = self.parse_with_cte_then_select()?;
24307        match inner {
24308            Statement::Select(s) => Ok(s),
24309            other => Err(self.err(format!(
24310                "expected SELECT after WITH in a subquery, got {other:?}"
24311            ))),
24312        }
24313    }
24314
24315    /// True when the next token is the (unquoted) WITH keyword. WITH is
24316    /// reserved in PG, so a bare `with` can never be a column reference
24317    /// in these positions; a quoted `"with"` stays an identifier.
24318    fn peek_is_with_kw(&self) -> bool {
24319        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24320    }
24321
24322    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24323    /// `#[inline(never)]` keeps the large SelectStatement temporaries
24324    /// off parse_expr's recursive frame (the nesting-budget stack
24325    /// cliff — see the round-153 gate regression).
24326    #[inline(never)]
24327    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24328        if self.peek_is_with_kw() {
24329            self.advance();
24330            self.parse_nested_with_select()
24331        } else {
24332            match self.parse_select_stmt()? {
24333                Statement::Select(s) => Ok(s),
24334                other => Err(self.err(alloc::format!(
24335                    "expected SELECT inside ANY/ALL, got {other:?}"
24336                ))),
24337            }
24338        }
24339    }
24340
24341    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24342        if !matches!(self.peek(), Token::LParen) {
24343            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24344        }
24345        self.advance();
24346        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24347        let s = if self.peek_is_with_kw() {
24348            self.advance();
24349            self.parse_nested_with_select()?
24350        } else {
24351            let inner = self.parse_select_stmt()?;
24352            let Statement::Select(s) = inner else {
24353                unreachable!("parse_select_stmt returns Select")
24354            };
24355            s
24356        };
24357        if !matches!(self.peek(), Token::RParen) {
24358            return Err(self.err(format!(
24359                "expected ')' after EXISTS-subquery, got {:?}",
24360                self.peek()
24361            )));
24362        }
24363        self.advance();
24364        Ok(Expr::Exists {
24365            subquery: Box::new(s),
24366            negated,
24367        })
24368    }
24369
24370    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24371        self.advance(); // IN
24372        if !matches!(self.peek(), Token::LParen) {
24373            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24374        }
24375        self.advance();
24376        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24377        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24378        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24379            let s = if self.peek_is_with_kw() {
24380                self.advance();
24381                self.parse_nested_with_select()?
24382            } else {
24383                let inner = self.parse_select_stmt()?;
24384                let Statement::Select(s) = inner else {
24385                    unreachable!("parse_select_stmt always returns Statement::Select")
24386                };
24387                s
24388            };
24389            if !matches!(self.peek(), Token::RParen) {
24390                return Err(self.err(format!(
24391                    "expected ')' after IN-subquery, got {:?}",
24392                    self.peek()
24393                )));
24394            }
24395            self.advance();
24396            return Ok(Expr::InSubquery {
24397                expr: Box::new(expr),
24398                subquery: Box::new(s),
24399                negated,
24400            });
24401        }
24402        let mut elements = Vec::new();
24403        if !matches!(self.peek(), Token::RParen) {
24404            loop {
24405                elements.push(self.parse_expr(0)?);
24406                match self.peek() {
24407                    Token::Comma => {
24408                        self.advance();
24409                    }
24410                    Token::RParen => break,
24411                    other => {
24412                        return Err(
24413                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24414                        );
24415                    }
24416                }
24417            }
24418        }
24419        self.advance(); // ')'
24420        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24421        // left-deep OR-Eq chain: chain depth scaled with the element
24422        // count and overflowed the stack (eval + drop are recursive).
24423        if elements.is_empty() {
24424            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24425        }
24426        Ok(Expr::InList {
24427            expr: Box::new(expr),
24428            list: elements,
24429            negated,
24430        })
24431    }
24432
24433    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24434    /// already consumed by the caller. Elements must be numeric literals
24435    /// (with optional unary `-`); any compound expression is rejected at
24436    /// parse time so the runtime never needs to evaluate inside a vector.
24437    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24438    /// has already consumed the `EXTRACT` token before calling us —
24439    /// we pick up at the opening `(`.
24440    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24441    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24442    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24443    /// per-column OR-fold of
24444    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24445    /// term)` so the existing FTS evaluator handles semantics.
24446    ///
24447    /// The mode modifier is accepted-and-ignored at v7.17 — all
24448    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24449    /// mode operators (`+foo -bar`) would need their own parser
24450    /// (Phase 2.2c); customers who hit them today already get a
24451    /// correct lexeme-match against the bare term, only without
24452    /// the +/- precedence the customer asked for.
24453    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24454        // Already at `MATCH`-consumed position; the dispatcher
24455        // confirmed the next token is `(`.
24456        if !matches!(self.peek(), Token::LParen) {
24457            return Err(self.err(alloc::format!(
24458                "expected '(' after MATCH, got {:?}",
24459                self.peek()
24460            )));
24461        }
24462        self.advance();
24463        let mut cols: Vec<Expr> = Vec::new();
24464        loop {
24465            cols.push(self.parse_expr(0)?);
24466            match self.peek() {
24467                Token::Comma => {
24468                    self.advance();
24469                }
24470                Token::RParen => break,
24471                other => {
24472                    return Err(self.err(alloc::format!(
24473                        "expected ',' or ')' in MATCH column list, got {other:?}"
24474                    )));
24475                }
24476            }
24477        }
24478        self.advance(); // ')'
24479        // Expect AGAINST.
24480        match self.peek() {
24481            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24482                self.advance();
24483            }
24484            other => {
24485                return Err(self.err(alloc::format!(
24486                    "expected AGAINST after MATCH column list, got {other:?}"
24487                )));
24488            }
24489        }
24490        if !matches!(self.peek(), Token::LParen) {
24491            return Err(self.err(alloc::format!(
24492                "expected '(' after AGAINST, got {:?}",
24493                self.peek()
24494            )));
24495        }
24496        self.advance();
24497        // Read AGAINST's argument as a single primary token —
24498        // string literal, placeholder, or column-ref ident. We
24499        // can't call `parse_expr` / `parse_unary` here because
24500        // the postfix chain inside `parse_atom` would greedily
24501        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24502        // and fail at "expected '(' after IN". Customers always
24503        // write a literal or bound parameter in AGAINST, so this
24504        // restriction is non-blocking; the error path explains
24505        // the limit if a more complex expression shows up.
24506        let term = match self.advance() {
24507            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24508            Token::Placeholder(n) => Expr::Placeholder(n),
24509            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24510                qualifier: None,
24511                name: s,
24512            }),
24513            other => {
24514                return Err(self.err(alloc::format!(
24515                    "MATCH ... AGAINST(<term>) expects a string literal, \
24516                     bound parameter, or column ref, got {other:?}"
24517                )));
24518            }
24519        };
24520        // Optional mode tail — accept-and-ignore at v7.17:
24521        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24522        //   IN BOOLEAN MODE
24523        //   WITH QUERY EXPANSION
24524        loop {
24525            match self.peek() {
24526                // IN lexes as a reserved Token::In, not an ident,
24527                // so it gets its own arm.
24528                Token::In => {
24529                    self.advance();
24530                }
24531                Token::Ident(s) | Token::QuotedIdent(s)
24532                    if s.eq_ignore_ascii_case("natural")
24533                        || s.eq_ignore_ascii_case("language")
24534                        || s.eq_ignore_ascii_case("boolean")
24535                        || s.eq_ignore_ascii_case("mode")
24536                        || s.eq_ignore_ascii_case("with")
24537                        || s.eq_ignore_ascii_case("query")
24538                        || s.eq_ignore_ascii_case("expansion") =>
24539                {
24540                    self.advance();
24541                }
24542                _ => break,
24543            }
24544        }
24545        if !matches!(self.peek(), Token::RParen) {
24546            return Err(self.err(alloc::format!(
24547                "expected ')' to close AGAINST, got {:?}",
24548                self.peek()
24549            )));
24550        }
24551        self.advance();
24552        // Build per-column `to_tsvector('simple', col) @@
24553        // plainto_tsquery('simple', term)` and OR-fold.
24554        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24555        let plainto = Expr::FunctionCall {
24556            name: String::from("plainto_tsquery"),
24557            args: alloc::vec![simple_lit(), term.clone()],
24558        };
24559        let mut folded: Option<Expr> = None;
24560        for col in cols {
24561            let to_tsv = Expr::FunctionCall {
24562                name: String::from("to_tsvector"),
24563                args: alloc::vec![simple_lit(), col],
24564            };
24565            let leaf = Expr::Binary {
24566                lhs: Box::new(to_tsv),
24567                op: crate::ast::BinOp::TsMatch,
24568                rhs: Box::new(plainto.clone()),
24569            };
24570            folded = Some(match folded {
24571                None => leaf,
24572                Some(prev) => Expr::Binary {
24573                    lhs: Box::new(prev),
24574                    op: crate::ast::BinOp::Or,
24575                    rhs: Box::new(leaf),
24576                },
24577            });
24578        }
24579        match folded {
24580            Some(e) => Ok(e),
24581            None => Err(self.err(String::from(
24582                "MATCH(...) AGAINST(...) requires at least one column",
24583            ))),
24584        }
24585    }
24586
24587    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24588        if !matches!(self.peek(), Token::LParen) {
24589            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24590        }
24591        self.advance();
24592        let field_name = self.expect_ident_like()?;
24593        let field = match field_name.to_ascii_lowercase().as_str() {
24594            // PG accepts the plural spellings (years/months/…/millenniums) as
24595            // aliases for the singular fields — its datetime unit table has both.
24596            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24597            "year" | "years" => ExtractField::Year,
24598            "month" | "months" => ExtractField::Month,
24599            "day" | "days" => ExtractField::Day,
24600            "hour" | "hours" => ExtractField::Hour,
24601            "minute" | "minutes" => ExtractField::Minute,
24602            "second" | "seconds" => ExtractField::Second,
24603            "microsecond" | "microseconds" => ExtractField::Microsecond,
24604            "epoch" => ExtractField::Epoch,
24605            "dow" => ExtractField::Dow,
24606            "isodow" => ExtractField::Isodow,
24607            "doy" => ExtractField::Doy,
24608            "week" | "weeks" => ExtractField::Week,
24609            "isoyear" => ExtractField::Isoyear,
24610            "quarter" => ExtractField::Quarter,
24611            "decade" | "decades" => ExtractField::Decade,
24612            "century" | "centuries" => ExtractField::Century,
24613            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24614            "julian" => ExtractField::Julian,
24615            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24616            "timezone" => ExtractField::Timezone,
24617            "timezone_hour" => ExtractField::TimezoneHour,
24618            "timezone_minute" => ExtractField::TimezoneMinute,
24619            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24620            // reports an unknown one with the source type (22023); carry the
24621            // raw name so eval can word it.
24622            other => ExtractField::Other(alloc::string::String::from(other)),
24623        };
24624        if !matches!(self.peek(), Token::From) {
24625            return Err(self.err(format!(
24626                "expected FROM after EXTRACT field, got {:?}",
24627                self.peek()
24628            )));
24629        }
24630        self.advance();
24631        let source = self.parse_expr(0)?;
24632        if !matches!(self.peek(), Token::RParen) {
24633            return Err(self.err(format!(
24634                "expected ')' to close EXTRACT, got {:?}",
24635                self.peek()
24636            )));
24637        }
24638        self.advance();
24639        Ok(Expr::Extract {
24640            field,
24641            source: Box::new(source),
24642        })
24643    }
24644
24645    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24646    /// is already consumed; we expect a single string literal next and
24647    /// resolve it into `Literal::Interval` at parse time so the engine
24648    /// never has to re-tokenise inside the string.
24649    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24650    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24651    /// is the SQL-standard form and is left to the path below.
24652    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24653        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24654        let (offset, sign) = match self.peek() {
24655            Token::Minus => (1, "-"),
24656            _ => (0, ""),
24657        };
24658        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24659            return None;
24660        };
24661        self.tokens
24662            .get(self.pos + offset + 1)
24663            .filter(|t| mysql_interval_unit(t).is_some())?;
24664        Some((alloc::format!("{sign}{n}"), offset + 1))
24665    }
24666
24667    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24668    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24669    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24670    ///
24671    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24672    /// this by parsing the group and then restoring `self.pos` — which could
24673    /// never have worked, because `advance()` DESTROYS the token it returns
24674    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24675    /// inert only because both branches errored back then.
24676    fn interval_paren_is_quantity(&self) -> bool {
24677        let mut depth = 0usize;
24678        let mut saw_top_level_comma = false;
24679        let mut i = self.pos;
24680        while let Some(tok) = self.tokens.get(i) {
24681            match tok {
24682                Token::LParen => depth += 1,
24683                Token::RParen => {
24684                    depth = depth.saturating_sub(1);
24685                    if depth == 0 {
24686                        return !saw_top_level_comma
24687                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24688                                .is_some();
24689                    }
24690                }
24691                // A comma directly inside the outermost parens means the
24692                // argument list of the INTERVAL() function.
24693                Token::Comma if depth == 1 => saw_top_level_comma = true,
24694                Token::Eof => return false,
24695                _ => {}
24696            }
24697            i += 1;
24698        }
24699        false
24700    }
24701
24702    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24703        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24704        // (the index of the last Ni ≤ N), distinct from the interval literal.
24705        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24706        // is decided by a non-destructive lookahead (round 422) before either
24707        // branch consumes anything. MySQL only.
24708        if self.mysql_dialect
24709            && matches!(self.peek(), Token::LParen)
24710            && !self.interval_paren_is_quantity()
24711        {
24712            self.advance(); // (
24713            let mut args = Vec::new();
24714            if !matches!(self.peek(), Token::RParen) {
24715                loop {
24716                    args.push(self.parse_expr(0)?);
24717                    if matches!(self.peek(), Token::Comma) {
24718                        self.advance();
24719                        continue;
24720                    }
24721                    break;
24722                }
24723            }
24724            if !matches!(self.peek(), Token::RParen) {
24725                return Err(self.err(alloc::format!(
24726                    "expected ')' after INTERVAL() arguments, got {:?}",
24727                    self.peek()
24728                )));
24729            }
24730            self.advance(); // )
24731            return Ok(Expr::FunctionCall {
24732                name: alloc::string::String::from("interval"),
24733                args,
24734            });
24735        }
24736        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24737        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24738        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24739        // writes every date arithmetic there is, and it did not parse at
24740        // all. PG rejects the unquoted form outright (`syntax error at or
24741        // near "1"`, measured), so it is taken only in the MySQL dialect —
24742        // PG's own `INTERVAL '1' DAY` is untouched below.
24743        if self.mysql_dialect
24744            && let Some((text, consume)) = self.peek_unquoted_interval_count()
24745        {
24746            for _ in 0..consume {
24747                self.advance(); // the optional `-` and the number
24748            }
24749            let Some(unit) = mysql_interval_unit(self.peek()) else {
24750                return Err(self.err(alloc::format!(
24751                    "expected an interval unit after INTERVAL {text}, got {:?}",
24752                    self.peek()
24753                )));
24754            };
24755            self.advance(); // the unit
24756            let (months, days, micros) = scale_mysql_interval(&text, unit)
24757                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24758            return Ok(Expr::Literal(Literal::Interval {
24759                months,
24760                days,
24761                micros,
24762                // The canonical rendering, so Display round-trips into a
24763                // form both dialects read back.
24764                text: alloc::format!("{text} {unit}"),
24765            }));
24766        }
24767        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24768        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24769        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24770        // Those cannot fold into a compile-time `Literal::Interval`, so they
24771        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24772        // builtin, which builds the value at run time (and yields NULL for a
24773        // NULL quantity, as MariaDB does). The literal path above still folds
24774        // the constant case — it is cheaper and round-trips through Display.
24775        //
24776        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24777        // MySQL's quoted spelling) keep the qualifier path below.
24778        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24779            let qty = self.parse_expr(0)?;
24780            let Some(unit) = mysql_interval_unit(self.peek()) else {
24781                return Err(self.err(alloc::format!(
24782                    "expected an interval unit after INTERVAL <expr>, got {:?}",
24783                    self.peek()
24784                )));
24785            };
24786            self.advance(); // the unit
24787            return Ok(make_interval_call(qty, unit));
24788        }
24789        let tok = self.advance();
24790        let Token::String(text) = tok else {
24791            return Err(self.err(format!(
24792                "expected string literal after INTERVAL, got {tok:?}"
24793            )));
24794        };
24795        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24796        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24797        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24798        // bare number means and the leading/trailing precision.
24799        let field1 = interval_field_of(self.peek());
24800        let qualifier = if let Some(f1) = field1 {
24801            self.advance();
24802            let f2 = if matches!(self.peek(), Token::To) {
24803                self.advance();
24804                let Some(f) = interval_field_of(self.peek()) else {
24805                    return Err(self.err(format!(
24806                        "expected an interval field after TO, got {:?}",
24807                        self.peek()
24808                    )));
24809                };
24810                self.advance();
24811                Some(f)
24812            } else {
24813                None
24814            };
24815            Some((f1, f2))
24816        } else {
24817            None
24818        };
24819        let (months, days, micros) = match qualifier {
24820            Some(q) => interpret_qualified_interval(&text, q),
24821            None => parse_interval_text(&text),
24822        }
24823        .ok_or_else(|| ParseError {
24824            message: format!(
24825                "cannot parse INTERVAL {text:?}; \
24826                     expected `<n> <unit> [<n> <unit> ...]` with units \
24827                     microsecond[s], millisecond[s], second[s], minute[s], \
24828                     hour[s], day[s], week[s], month[s], year[s]"
24829            ),
24830            token_pos: self.consumed_pos(),
24831        })?;
24832        Ok(Expr::Literal(Literal::Interval {
24833            months,
24834            days,
24835            micros,
24836            text,
24837        }))
24838    }
24839
24840    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24841    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24842    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24843    /// than a pgvector literal.
24844    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24845        self.advance(); // consume `[`
24846        let mut items: Vec<Expr> = Vec::new();
24847        if !matches!(self.peek(), Token::RBracket) {
24848            loop {
24849                if matches!(self.peek(), Token::LBracket) {
24850                    items.push(self.parse_array_bracket_body()?);
24851                } else {
24852                    items.push(self.parse_expr(0)?);
24853                }
24854                match self.peek() {
24855                    Token::Comma => {
24856                        self.advance();
24857                    }
24858                    Token::RBracket => break,
24859                    other => {
24860                        return Err(self.err(alloc::format!(
24861                            "expected ',' or ']' in array literal, got {other:?}"
24862                        )));
24863                    }
24864                }
24865            }
24866        }
24867        self.advance(); // consume `]`
24868        Ok(Expr::Array(items))
24869    }
24870
24871    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24872        let mut elems = Vec::new();
24873        if matches!(self.peek(), Token::RBracket) {
24874            self.advance();
24875            return Ok(Expr::Literal(Literal::Vector(elems)));
24876        }
24877        loop {
24878            let e = self.parse_expr(0)?;
24879            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24880                message: format!("vector element must be a numeric literal, got {e:?}"),
24881                token_pos: self.pos,
24882            })?;
24883            elems.push(x);
24884            match self.peek() {
24885                Token::Comma => {
24886                    self.advance();
24887                }
24888                Token::RBracket => {
24889                    self.advance();
24890                    break;
24891                }
24892                other => {
24893                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24894                }
24895            }
24896        }
24897        Ok(Expr::Literal(Literal::Vector(elems)))
24898    }
24899
24900    /// Atom that started with an identifier: could be `t.col`, `col`, or
24901    /// `func(arg, ...)`. Detect each shape by looking at the next token.
24902    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24903    /// [, ...])`. Caller has already consumed `OVER`. Either clause
24904    /// is optional; an empty `()` is also legal (PG semantics).
24905    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24906    /// modifier between `name(args)` and `OVER (...)`. Default is
24907    /// `Respect`. Unrecognised idents leave the stream unchanged.
24908    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24909        let Token::Ident(s) = self.peek().clone() else {
24910            return NullTreatment::Respect;
24911        };
24912        let is_ignore = s.eq_ignore_ascii_case("ignore");
24913        let is_respect = s.eq_ignore_ascii_case("respect");
24914        if !is_ignore && !is_respect {
24915            return NullTreatment::Respect;
24916        }
24917        // Lookahead for NULLS — only consume both tokens together.
24918        // pos+1 must hold a "nulls" ident.
24919        if self.pos + 1 < self.tokens.len()
24920            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24921            && s2.eq_ignore_ascii_case("nulls")
24922        {
24923            self.advance();
24924            self.advance();
24925            return if is_ignore {
24926                NullTreatment::Ignore
24927            } else {
24928                NullTreatment::Respect
24929            };
24930        }
24931        NullTreatment::Respect
24932    }
24933
24934    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24935    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24936    /// (same shape as the `OVER` tail). Consumes the whole clause and
24937    /// returns the predicate; returns `None` when no `FILTER` follows.
24938    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24939        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24940            return Ok(None);
24941        };
24942        if !s.eq_ignore_ascii_case("filter") {
24943            return Ok(None);
24944        }
24945        self.advance(); // FILTER
24946        if !matches!(self.peek(), Token::LParen) {
24947            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24948        }
24949        self.advance(); // (
24950        if !matches!(self.peek(), Token::Where) {
24951            return Err(self.err(format!(
24952                "expected WHERE inside FILTER (...), got {:?}",
24953                self.peek()
24954            )));
24955        }
24956        self.advance(); // WHERE
24957        let cond = self.parse_expr(0)?;
24958        if !matches!(self.peek(), Token::RParen) {
24959            return Err(self.err(format!(
24960                "expected ')' to close FILTER (WHERE ...), got {:?}",
24961                self.peek()
24962            )));
24963        }
24964        self.advance(); // )
24965        Ok(Some(Box::new(cond)))
24966    }
24967
24968    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24969    /// the separator as the aggregate's second argument, which is the
24970    /// shape `string_agg` already takes. Returns whether one was there.
24971    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24972        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24973            return Ok(false);
24974        }
24975        self.advance();
24976        let Token::String(sep) = self.peek().clone() else {
24977            return Err(self.err(alloc::format!(
24978                "expected a string literal after SEPARATOR, got {:?}",
24979                self.peek()
24980            )));
24981        };
24982        self.advance();
24983        args.push(Expr::Literal(Literal::String(sep)));
24984        Ok(true)
24985    }
24986
24987    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24988    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24989    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24990    /// keys, or an empty vec when no `WITHIN GROUP` follows.
24991    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24992        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24993            return Ok(Vec::new());
24994        };
24995        if !s.eq_ignore_ascii_case("within") {
24996            return Ok(Vec::new());
24997        }
24998        self.advance(); // WITHIN
24999        if !matches!(self.peek(), Token::Group) {
25000            return Err(self.err(format!(
25001                "expected GROUP after WITHIN, got {:?}",
25002                self.peek()
25003            )));
25004        }
25005        self.advance(); // GROUP
25006        if !matches!(self.peek(), Token::LParen) {
25007            return Err(self.err(format!(
25008                "expected '(' after WITHIN GROUP, got {:?}",
25009                self.peek()
25010            )));
25011        }
25012        self.advance(); // (
25013        if !matches!(self.peek(), Token::Order) {
25014            return Err(self.err(format!(
25015                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25016                self.peek()
25017            )));
25018        }
25019        self.advance(); // ORDER
25020        if !self.peek_is_by() {
25021            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25022        }
25023        self.advance(); // BY
25024        let mut keys: Vec<OrderBy> = Vec::new();
25025        loop {
25026            // v7.39 (round 691) — save/restore, the discipline this parser
25027            // already uses around `pending_sample_preds`, so a subquery inside
25028            // a key neither inherits nor leaks the channel.
25029            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25030            let saved_coll = self.order_key_collation.take();
25031            let parsed = self.parse_expr(0);
25032            self.in_order_by_key = saved_flag;
25033            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25034            let expr = parsed?;
25035            let desc = if matches!(self.peek(), Token::Desc) {
25036                self.advance();
25037                true
25038            } else if matches!(self.peek(), Token::Asc) {
25039                self.advance();
25040                false
25041            } else {
25042                false
25043            };
25044            let nulls_first = self.parse_optional_nulls_placement()?;
25045            keys.push(OrderBy {
25046                expr,
25047                desc,
25048                nulls_first,
25049                collation,
25050            });
25051            if matches!(self.peek(), Token::Comma) {
25052                self.advance();
25053            } else {
25054                break;
25055            }
25056        }
25057        if !matches!(self.peek(), Token::RParen) {
25058            return Err(self.err(format!(
25059                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25060                self.peek()
25061            )));
25062        }
25063        self.advance(); // )
25064        Ok(keys)
25065    }
25066
25067    /// No frame clause is supported.
25068    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25069    fn parse_over_clause(
25070        &mut self,
25071    ) -> Result<
25072        (
25073            Vec<Expr>,
25074            Vec<(Expr, bool, Option<bool>)>,
25075            Option<WindowFrame>,
25076        ),
25077        ParseError,
25078    > {
25079        // `OVER w` — a named-window reference. The WINDOW clause
25080        // parses after the select list, so the name rides out as a
25081        // marker in partition_by; parse_bare_select substitutes the
25082        // definition once the clause is known.
25083        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25084            let name = w.clone();
25085            self.advance();
25086            return Ok((
25087                alloc::vec![Expr::Column(crate::ast::ColumnName {
25088                    qualifier: Some("__named_window__".to_string()),
25089                    name,
25090                })],
25091                Vec::new(),
25092                None,
25093            ));
25094        }
25095        if !matches!(self.peek(), Token::LParen) {
25096            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25097        }
25098        self.advance();
25099        let mut partition_by = Vec::new();
25100        let mut order_by = Vec::new();
25101        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25102        // window, refined in place. PG's rules (probed against 18.4) differ
25103        // from the bare `OVER w1` form, so the reference rides out under its
25104        // own marker and `substitute_named_windows` applies them. The base
25105        // name is any leading identifier that isn't a window-spec keyword.
25106        let base_window = match self.peek() {
25107            Token::Ident(s) | Token::QuotedIdent(s)
25108                if !s.eq_ignore_ascii_case("partition")
25109                    && !s.eq_ignore_ascii_case("rows")
25110                    && !s.eq_ignore_ascii_case("range")
25111                    && !s.eq_ignore_ascii_case("groups") =>
25112            {
25113                let n = s.clone();
25114                self.advance();
25115                Some(n)
25116            }
25117            _ => None,
25118        };
25119        // PARTITION BY ?
25120        // v7.37.6-B promoted PARTITION to a reserved keyword
25121        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25122        // `Token::Ident("partition")`. Accept both so older sources
25123        // and the new lexer surface land on the same path.
25124        let is_partition_kw = match self.peek() {
25125            Token::Partition => true,
25126            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25127            _ => false,
25128        };
25129        if is_partition_kw {
25130            self.advance();
25131            if !self.peek_is_by() {
25132                return Err(self.err(format!(
25133                    "expected BY after PARTITION, got {:?}",
25134                    self.peek()
25135                )));
25136            }
25137            self.advance();
25138            loop {
25139                partition_by.push(self.parse_expr(0)?);
25140                if matches!(self.peek(), Token::Comma) {
25141                    self.advance();
25142                    continue;
25143                }
25144                break;
25145            }
25146        }
25147        // ORDER BY ?
25148        if matches!(self.peek(), Token::Order) {
25149            self.advance();
25150            if !self.peek_is_by() {
25151                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25152            }
25153            self.advance();
25154            loop {
25155                let e = self.parse_expr(0)?;
25156                let desc = if matches!(self.peek(), Token::Desc) {
25157                    self.advance();
25158                    true
25159                } else if matches!(self.peek(), Token::Asc) {
25160                    self.advance();
25161                    false
25162                } else {
25163                    false
25164                };
25165                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25166                let nulls_first = self.parse_optional_nulls_placement()?;
25167                order_by.push((e, desc, nulls_first));
25168                if matches!(self.peek(), Token::Comma) {
25169                    self.advance();
25170                    continue;
25171                }
25172                break;
25173            }
25174        }
25175        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25176        // Both keywords come through the lexer as identifiers; match
25177        // case-insensitively.
25178        let mut frame: Option<WindowFrame> = None;
25179        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25180            let kind = if s.eq_ignore_ascii_case("rows") {
25181                Some(FrameKind::Rows)
25182            } else if s.eq_ignore_ascii_case("range") {
25183                Some(FrameKind::Range)
25184            } else if s.eq_ignore_ascii_case("groups") {
25185                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25186                Some(FrameKind::Groups)
25187            } else {
25188                None
25189            };
25190            if let Some(kind) = kind {
25191                self.advance();
25192                frame = Some(self.parse_frame_tail(kind)?);
25193            }
25194        }
25195        if !matches!(self.peek(), Token::RParen) {
25196            return Err(self.err(format!(
25197                "expected ')' to close OVER clause, got {:?}",
25198                self.peek()
25199            )));
25200        }
25201        self.advance();
25202        if let Some(base) = base_window {
25203            // A copy may refine but never override the base's partitioning
25204            // (PG rejects it outright, before looking the name up).
25205            if !partition_by.is_empty() {
25206                return Err(self.err(alloc::format!(
25207                    "cannot override PARTITION BY clause of window \"{base}\""
25208                )));
25209            }
25210            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25211                qualifier: Some("__named_window_ref__".to_string()),
25212                name: base,
25213            })];
25214        }
25215        Ok((partition_by, order_by, frame))
25216    }
25217
25218    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25219    /// or `RANGE` keyword was just consumed. Accepts both
25220    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25221    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25222    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25223    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25224        let (start, end) = if matches!(self.peek(), Token::Between) {
25225            self.advance();
25226            let start = self.parse_frame_bound()?;
25227            if !matches!(self.peek(), Token::And) {
25228                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25229            }
25230            self.advance();
25231            let end = self.parse_frame_bound()?;
25232            (start, Some(end))
25233        } else {
25234            (self.parse_frame_bound()?, None)
25235        };
25236        let exclude = self.parse_frame_exclusion()?;
25237        Ok(WindowFrame {
25238            kind,
25239            start,
25240            end,
25241            exclude,
25242        })
25243    }
25244
25245    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25246    /// after a frame spec. NO OTHERS is the default no-op.
25247    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25248        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25249            return Ok(FrameExclusion::NoOthers);
25250        }
25251        self.advance(); // EXCLUDE
25252        match self.peek() {
25253            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25254                self.advance();
25255                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25256                    return Err(self.err(format!(
25257                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25258                        self.peek()
25259                    )));
25260                }
25261                self.advance();
25262                Ok(FrameExclusion::CurrentRow)
25263            }
25264            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25265            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25266            // Without this arm it fell to the catch-all, whose message
25267            // self-contradictingly listed GROUP as expected.
25268            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25269                self.advance();
25270                Ok(FrameExclusion::Group)
25271            }
25272            Token::Group => {
25273                self.advance();
25274                Ok(FrameExclusion::Group)
25275            }
25276            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25277                self.advance();
25278                Ok(FrameExclusion::Ties)
25279            }
25280            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25281                self.advance();
25282                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25283                    return Err(self.err(format!(
25284                        "expected OTHERS after EXCLUDE NO, got {:?}",
25285                        self.peek()
25286                    )));
25287                }
25288                self.advance();
25289                Ok(FrameExclusion::NoOthers)
25290            }
25291            other => Err(self.err(format!(
25292                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25293            ))),
25294        }
25295    }
25296
25297    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25298    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25299    /// `UNBOUNDED FOLLOWING`.
25300    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25301        // Interval-typed offset for a value-based RANGE frame over a
25302        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25303        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25304        // PRECEDING`.
25305        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25306            let dir = self.expect_ident_like()?;
25307            return if dir.eq_ignore_ascii_case("preceding") {
25308                Ok(FrameBound::IntervalPreceding {
25309                    months,
25310                    days,
25311                    micros,
25312                })
25313            } else if dir.eq_ignore_ascii_case("following") {
25314                Ok(FrameBound::IntervalFollowing {
25315                    months,
25316                    days,
25317                    micros,
25318                })
25319            } else {
25320                Err(self.err(format!(
25321                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25322                )))
25323            };
25324        }
25325        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25326        if let Token::Integer(n) = *self.peek() {
25327            self.advance();
25328            let n: u64 = u64::try_from(n).map_err(|_| {
25329                self.err(format!(
25330                    "invalid frame offset {n} — expected non-negative integer"
25331                ))
25332            })?;
25333            let dir = self.expect_ident_like()?;
25334            return if dir.eq_ignore_ascii_case("preceding") {
25335                Ok(FrameBound::OffsetPreceding(n))
25336            } else if dir.eq_ignore_ascii_case("following") {
25337                Ok(FrameBound::OffsetFollowing(n))
25338            } else {
25339                Err(self.err(format!(
25340                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25341                )))
25342            };
25343        }
25344        let first = self.expect_ident_like()?;
25345        if first.eq_ignore_ascii_case("unbounded") {
25346            let dir = self.expect_ident_like()?;
25347            return if dir.eq_ignore_ascii_case("preceding") {
25348                Ok(FrameBound::UnboundedPreceding)
25349            } else if dir.eq_ignore_ascii_case("following") {
25350                Ok(FrameBound::UnboundedFollowing)
25351            } else {
25352                Err(self.err(format!(
25353                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25354                )))
25355            };
25356        }
25357        if first.eq_ignore_ascii_case("current") {
25358            let row = self.expect_ident_like()?;
25359            if !row.eq_ignore_ascii_case("row") {
25360                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25361            }
25362            return Ok(FrameBound::CurrentRow);
25363        }
25364        Err(self.err(format!(
25365            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25366        )))
25367    }
25368
25369    /// Detect and consume a leading interval offset in a frame bound —
25370    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25371    /// `(months, days, micros)`. Leaves the cursor on the trailing
25372    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25373    /// when the next tokens are not an interval offset.
25374    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25375        // Shape A — `INTERVAL '1 day'`.
25376        if matches!(self.peek(), Token::Interval) {
25377            self.advance(); // INTERVAL
25378            let atom = self.parse_interval_atom()?;
25379            if let Expr::Literal(Literal::Interval {
25380                months,
25381                days,
25382                micros,
25383                ..
25384            }) = atom
25385            {
25386                return Ok(Some((months, days, micros)));
25387            }
25388            return Err(self.err("expected an interval literal in frame offset".to_string()));
25389        }
25390        // Shape B — `'1 day'::interval`. Look ahead for the exact
25391        // string / `::` / interval-target triple before committing.
25392        if let Token::String(text) = self.peek() {
25393            let target_is_interval = match self.tokens.get(self.pos + 2) {
25394                Some(Token::Interval) => true,
25395                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25396                _ => false,
25397            };
25398            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25399                && target_is_interval;
25400            if is_cast {
25401                let text = text.clone();
25402                self.advance(); // string
25403                self.advance(); // ::
25404                self.advance(); // interval
25405                let parts = parse_interval_text(&text).ok_or_else(|| {
25406                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25407                })?;
25408                return Ok(Some(parts));
25409            }
25410        }
25411        Ok(None)
25412    }
25413
25414    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25415        if matches!(self.peek(), Token::Dot) {
25416            self.advance();
25417            let name = self.expect_ident_like()?;
25418            // v7.14.0 — schema-qualified function call
25419            // `<schema>.<fn>(args)`. PG dumps emit
25420            // `pg_catalog.set_config(...)` in the preamble. SPG
25421            // is single-namespace: drop the schema prefix and
25422            // route the dispatch on the bare function name.
25423            if matches!(self.peek(), Token::LParen) {
25424                return self.finish_ident_atom(name);
25425            }
25426            return Ok(Expr::Column(ColumnName {
25427                qualifier: Some(first),
25428                name,
25429            }));
25430        }
25431        if matches!(self.peek(), Token::LParen) {
25432            self.advance();
25433            // `COUNT(*)` — special-cased here because `*` isn't a normal
25434            // expression token. Lower-case match on `first` since the lexer
25435            // folds identifiers.
25436            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25437                self.advance();
25438                if !matches!(self.peek(), Token::RParen) {
25439                    return Err(self.err(format!(
25440                        "expected ')' after COUNT(*), got {:?}",
25441                        self.peek()
25442                    )));
25443                }
25444                self.advance();
25445                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25446                let filter = self.parse_filter_clause()?;
25447                // v4.12: COUNT(*) OVER (...) — same window tail.
25448                let null_treatment = self.parse_null_treatment_modifier();
25449                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25450                    && s.eq_ignore_ascii_case("over")
25451                {
25452                    self.advance();
25453                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25454                    return Ok(Expr::WindowFunction {
25455                        name: "count_star".into(),
25456                        args: Vec::new(),
25457                        partition_by,
25458                        order_by,
25459                        frame,
25460                        null_treatment,
25461                        filter,
25462                    });
25463                }
25464                if let Some(filter) = filter {
25465                    return Ok(Expr::AggregateOrdered {
25466                        call: Box::new(Expr::FunctionCall {
25467                            name: "count_star".into(),
25468                            args: Vec::new(),
25469                        }),
25470                        order_by: Vec::new(),
25471                        distinct: false,
25472                        filter: Some(filter),
25473                    });
25474                }
25475                return Ok(Expr::FunctionCall {
25476                    name: "count_star".into(),
25477                    args: Vec::new(),
25478                });
25479            }
25480            // Function call. PG-style: zero-or-more comma-separated args.
25481            let mut args = Vec::new();
25482            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25483            // Names are collected in lock-step with `args` and resolved to
25484            // positional order after the loop (the AST stays positional).
25485            let mut arg_names: Vec<Option<String>> = Vec::new();
25486            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25487            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25488            // seen, so the value arguments before it can be folded.
25489            let mut saw_separator = false;
25490            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25491            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25492            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25493            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25494                self.advance();
25495                true
25496            } else if matches!(self.peek(), Token::All) {
25497                self.advance();
25498                false
25499            } else {
25500                false
25501            };
25502            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25503            // TIMESTAMPDIFF take a bare unit keyword as the first
25504            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25505            // bare type keyword (DATE / TIME / DATETIME); lower them
25506            // onto string literals so the evaluator sees plain text.
25507            if ((first.eq_ignore_ascii_case("timestampadd")
25508                || first.eq_ignore_ascii_case("timestampdiff"))
25509                && matches!(self.peek(), Token::Ident(u) if matches!(
25510                    u.to_ascii_lowercase().as_str(),
25511                    "microsecond" | "second" | "minute" | "hour" | "day"
25512                        | "week" | "month" | "quarter" | "year"
25513                )))
25514                || (first.eq_ignore_ascii_case("get_format")
25515                    && matches!(self.peek(), Token::Ident(u) if matches!(
25516                        u.to_ascii_lowercase().as_str(),
25517                        "date" | "time" | "datetime" | "timestamp"
25518                    )))
25519            {
25520                if let Token::Ident(u) = self.peek() {
25521                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25522                }
25523                self.advance();
25524                if matches!(self.peek(), Token::Comma) {
25525                    self.advance();
25526                }
25527            }
25528            // `ROW(a, b, …)` keyword constructor. Followed by a
25529            // comparison operator or [NOT] IN it joins the paren
25530            // row-constructor machinery (fieldwise parse-time
25531            // expansion); bare, it stays a `row` call the evaluator
25532            // renders as PG record text.
25533            if first.eq_ignore_ascii_case("row") {
25534                let mut row_items = Vec::new();
25535                if !matches!(self.peek(), Token::RParen) {
25536                    loop {
25537                        row_items.push(self.parse_expr(0)?);
25538                        match self.peek() {
25539                            Token::Comma => {
25540                                self.advance();
25541                            }
25542                            Token::RParen => break,
25543                            other => {
25544                                return Err(self.err(format!(
25545                                    "expected ',' or ')' in ROW(...), got {other:?}"
25546                                )));
25547                            }
25548                        }
25549                    }
25550                }
25551                self.advance(); // ')'
25552                let comparison_follows = matches!(
25553                    self.peek(),
25554                    Token::Eq
25555                        | Token::NotEq
25556                        | Token::Lt
25557                        | Token::LtEq
25558                        | Token::Gt
25559                        | Token::GtEq
25560                        | Token::In
25561                ) || (matches!(self.peek(), Token::Not)
25562                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25563                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25564                if comparison_follows && !row_items.is_empty() {
25565                    return self.parse_row_comparison_tail(row_items);
25566                }
25567                return Ok(Expr::FunctionCall {
25568                    name: String::from("row"),
25569                    args: row_items,
25570                });
25571            }
25572            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25573            // the parse-mode keyword introduces the source text. SPG
25574            // carries XML as text, so both modes lower to __xmlparse(expr)
25575            // which validates well-formedness and returns Value::Xml.
25576            if first.eq_ignore_ascii_case("xmlparse")
25577                && matches!(self.peek(), Token::Ident(kw)
25578                    if kw.eq_ignore_ascii_case("document")
25579                        || kw.eq_ignore_ascii_case("content"))
25580            {
25581                let mode = match self.advance() {
25582                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25583                    _ => unreachable!("peeked an ident"),
25584                };
25585                let src = self.parse_expr(0)?;
25586                if !matches!(self.peek(), Token::RParen) {
25587                    return Err(self.err(format!(
25588                        "expected ')' to close XMLPARSE, got {:?}",
25589                        self.peek()
25590                    )));
25591                }
25592                self.advance();
25593                return Ok(Expr::FunctionCall {
25594                    name: String::from("__xmlparse"),
25595                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25596                });
25597            }
25598            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25599            // keyword introduces the element name (a bare or quoted
25600            // identifier), then optional content expressions. Lower to a
25601            // plain `xmlelement(name_text, content …)` call.
25602            if first.eq_ignore_ascii_case("xmlelement")
25603                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25604            {
25605                self.advance(); // consume NAME
25606                let elem_name = match self.peek().clone() {
25607                    Token::Ident(n) | Token::QuotedIdent(n) => {
25608                        self.advance();
25609                        n
25610                    }
25611                    other => {
25612                        return Err(self.err(format!(
25613                            "expected element name after XMLELEMENT NAME, got {other:?}"
25614                        )));
25615                    }
25616                };
25617                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25618                while matches!(self.peek(), Token::Comma) {
25619                    self.advance();
25620                    args.push(self.parse_expr(0)?);
25621                }
25622                if !matches!(self.peek(), Token::RParen) {
25623                    return Err(self.err(format!(
25624                        "expected ')' to close XMLELEMENT, got {:?}",
25625                        self.peek()
25626                    )));
25627                }
25628                self.advance();
25629                return Ok(Expr::FunctionCall {
25630                    name: String::from("xmlelement"),
25631                    args,
25632                });
25633            }
25634            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25635            // becomes a `<name>value</name>` element; a bare column infers its
25636            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25637            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25638                let mut args: Vec<Expr> = Vec::new();
25639                loop {
25640                    let val = self.parse_expr(0)?;
25641                    let name = if matches!(self.peek(), Token::As) {
25642                        self.advance();
25643                        match self.peek().clone() {
25644                            Token::Ident(n) | Token::QuotedIdent(n) => {
25645                                self.advance();
25646                                n
25647                            }
25648                            other => {
25649                                return Err(self.err(format!(
25650                                    "expected name after AS in XMLFOREST, got {other:?}"
25651                                )));
25652                            }
25653                        }
25654                    } else if let Expr::Column(c) = &val {
25655                        c.name.clone()
25656                    } else {
25657                        return Err(
25658                            self.err("XMLFOREST element without a column name needs AS".into())
25659                        );
25660                    };
25661                    args.push(Expr::Literal(Literal::String(name)));
25662                    args.push(val);
25663                    if matches!(self.peek(), Token::Comma) {
25664                        self.advance();
25665                    } else {
25666                        break;
25667                    }
25668                }
25669                if !matches!(self.peek(), Token::RParen) {
25670                    return Err(self.err(format!(
25671                        "expected ')' to close XMLFOREST, got {:?}",
25672                        self.peek()
25673                    )));
25674                }
25675                self.advance();
25676                return Ok(Expr::FunctionCall {
25677                    name: String::from("xmlforest"),
25678                    args,
25679                });
25680            }
25681            // SQL-standard `POSITION(sub IN str)` — lowers onto
25682            // strpos(str, sub). IN is the argument separator here,
25683            // so the needle parses with the IN-tail suppressed.
25684            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25685                let saved = self.suppress_in_tail;
25686                self.suppress_in_tail = true;
25687                let needle = self.parse_expr(0);
25688                self.suppress_in_tail = saved;
25689                let needle = needle?;
25690                if matches!(self.peek(), Token::In) {
25691                    self.advance();
25692                    let haystack = self.parse_expr(0)?;
25693                    if !matches!(self.peek(), Token::RParen) {
25694                        return Err(self.err(format!(
25695                            "expected ')' to close POSITION, got {:?}",
25696                            self.peek()
25697                        )));
25698                    }
25699                    self.advance();
25700                    return Ok(Expr::FunctionCall {
25701                        name: String::from("strpos"),
25702                        args: alloc::vec![haystack, needle],
25703                    });
25704                }
25705                // position(sub, str) comma form (incl. bytea) —
25706                // hand the parsed first arg to the generic list.
25707                args.push(needle);
25708                if matches!(self.peek(), Token::Comma) {
25709                    self.advance();
25710                }
25711            }
25712            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25713            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25714            // plain comma forms TRIM(str) / TRIM(str, chars) keep
25715            // riding the generic argument list below.
25716            if first.eq_ignore_ascii_case("trim") {
25717                let mode = match self.peek() {
25718                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25719                        self.advance();
25720                        Some("btrim")
25721                    }
25722                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25723                        self.advance();
25724                        Some("ltrim")
25725                    }
25726                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25727                        self.advance();
25728                        Some("rtrim")
25729                    }
25730                    _ => None,
25731                };
25732                if mode.is_some() || matches!(self.peek(), Token::From) {
25733                    // TRIM([mode] FROM str) — no strip-chars.
25734                    let chars = if matches!(self.peek(), Token::From) {
25735                        None
25736                    } else {
25737                        Some(self.parse_expr(0)?)
25738                    };
25739                    if !matches!(self.peek(), Token::From) {
25740                        return Err(self.err(format!(
25741                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25742                            self.peek()
25743                        )));
25744                    }
25745                    self.advance();
25746                    let target = self.parse_expr(0)?;
25747                    if !matches!(self.peek(), Token::RParen) {
25748                        return Err(
25749                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25750                        );
25751                    }
25752                    self.advance();
25753                    let mut trim_args = alloc::vec![target];
25754                    if let Some(c) = chars {
25755                        trim_args.push(c);
25756                    }
25757                    return Ok(Expr::FunctionCall {
25758                        name: String::from(mode.unwrap_or("btrim")),
25759                        args: trim_args,
25760                    });
25761                }
25762            }
25763            if !matches!(self.peek(), Token::RParen) {
25764                loop {
25765                    // v7.38 (read01, T14) — `argname => value` names this arg.
25766                    // v7.39 (read01 round 77) — `argname := value` is the same
25767                    // thing, and it is the spelling PG's own docs lead with. It
25768                    // was simply never lexed here, so every `f(x := 1)` died in
25769                    // the parser regardless of what `f` was.
25770                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25771                        (
25772                            Token::Ident(n) | Token::QuotedIdent(n),
25773                            Some(Token::FatArrow | Token::ColonEq),
25774                        ) => {
25775                            let name = n.clone();
25776                            self.advance(); // name
25777                            self.advance(); // => / :=
25778                            Some(name)
25779                        }
25780                        _ => None,
25781                    };
25782                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25783                    // array's elements into a variadic call's trailing args
25784                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25785                    // reserved, so it arrives as a bare ident before the arg.
25786                    let is_variadic = this_name.is_none()
25787                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25788                    if is_variadic {
25789                        self.advance();
25790                    }
25791                    let arg = self.parse_expr(0)?;
25792                    args.push(match &this_name {
25793                        // The callee's parameter names decide the slot, and a
25794                        // user function's live in the catalog. Carry the name
25795                        // to eval rather than guessing here.
25796                        Some(n) => Expr::NamedArg {
25797                            name: n.clone(),
25798                            expr: Box::new(arg),
25799                        },
25800                        None if is_variadic => Expr::Variadic(Box::new(arg)),
25801                        None => arg,
25802                    });
25803                    arg_names.push(this_name);
25804                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
25805                    // The `::` cast already worked; this lowers the
25806                    // function form onto the same Expr::Cast node.
25807                    if first.eq_ignore_ascii_case("cast")
25808                        && args.len() == 1
25809                        && matches!(self.peek(), Token::As)
25810                    {
25811                        self.advance();
25812                        let target = self.parse_cast_target()?;
25813                        if !matches!(self.peek(), Token::RParen) {
25814                            return Err(self.err(format!(
25815                                "expected ')' to close CAST, got {:?}",
25816                                self.peek()
25817                            )));
25818                        }
25819                        self.advance();
25820                        return Ok(Expr::Cast {
25821                            expr: Box::new(args.pop().expect("one arg")),
25822                            target,
25823                        });
25824                    }
25825                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25826                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25827                    // keywords; SPG's lexer makes them plain idents (so they'd be
25828                    // read as column refs). Lower the keyword to the string form
25829                    // the evaluator already accepts.
25830                    if first.eq_ignore_ascii_case("normalize")
25831                        && args.len() == 1
25832                        && matches!(self.peek(), Token::Comma)
25833                    {
25834                        let form = match self.tokens.get(self.pos + 1) {
25835                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25836                                let up = f.to_ascii_uppercase();
25837                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25838                            }
25839                            _ => None,
25840                        };
25841                        if let Some(up) = form {
25842                            self.advance(); // comma
25843                            self.advance(); // form keyword
25844                            args.push(Expr::Literal(Literal::String(up)));
25845                        }
25846                    }
25847                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25848                    // form. Desugars to the comma-list shape evaluator already
25849                    // handles. Triggered after the first arg when the function
25850                    // name is substring / substr and the next token is FROM
25851                    // (a reserved keyword in PG; SPG also reserves it).
25852                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25853                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25854                    // internal __substring_similar(str, pat, esc) call.
25855                    if (first.eq_ignore_ascii_case("substring")
25856                        || first.eq_ignore_ascii_case("substr"))
25857                        && args.len() == 1
25858                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25859                    {
25860                        self.advance(); // SIMILAR
25861                        let pattern = self.parse_expr(0)?;
25862                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25863                        {
25864                            return Err(self.err(format!(
25865                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25866                                self.peek()
25867                            )));
25868                        }
25869                        self.advance(); // ESCAPE
25870                        let esc = self.parse_expr(0)?;
25871                        if !matches!(self.peek(), Token::RParen) {
25872                            return Err(self.err(format!(
25873                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
25874                                self.peek()
25875                            )));
25876                        }
25877                        self.advance();
25878                        args.push(pattern);
25879                        args.push(esc);
25880                        return Ok(Expr::FunctionCall {
25881                            name: "__substring_similar".to_string(),
25882                            args,
25883                        });
25884                    }
25885                    if (first.eq_ignore_ascii_case("substring")
25886                        || first.eq_ignore_ascii_case("substr"))
25887                        && args.len() == 1
25888                        && matches!(self.peek(), Token::From | Token::For)
25889                    {
25890                        // `substring(str FROM pos [FOR len])`, or the FOR-only
25891                        // `substring(str FOR len)` which PG treats as FROM 1.
25892                        if matches!(self.peek(), Token::From) {
25893                            self.advance();
25894                            let start = self.parse_expr(0)?;
25895                            args.push(start);
25896                        } else {
25897                            args.push(Expr::Literal(Literal::Integer(1)));
25898                        }
25899                        if matches!(self.peek(), Token::For) {
25900                            self.advance();
25901                            let length = self.parse_expr(0)?;
25902                            args.push(length);
25903                        }
25904                        if !matches!(self.peek(), Token::RParen) {
25905                            return Err(self.err(format!(
25906                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25907                                self.peek()
25908                            )));
25909                        }
25910                        self.advance();
25911                        return Ok(Expr::FunctionCall {
25912                            name: first.to_ascii_lowercase(),
25913                            args,
25914                        });
25915                    }
25916                    // PG `overlay(str PLACING repl FROM n [FOR len])`
25917                    // syntactic form. Desugars to the `overlay(str,
25918                    // repl, n[, len])` comma-list shape the evaluator
25919                    // already implements. `PLACING` is not a reserved
25920                    // token in SPG, so it arrives as a bare Ident.
25921                    if first.eq_ignore_ascii_case("overlay")
25922                        && args.len() == 1
25923                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25924                    {
25925                        self.advance(); // consume PLACING
25926                        args.push(self.parse_expr(0)?); // replacement
25927                        if !matches!(self.peek(), Token::From) {
25928                            return Err(self.err(format!(
25929                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25930                                self.peek()
25931                            )));
25932                        }
25933                        self.advance();
25934                        args.push(self.parse_expr(0)?); // start position
25935                        if matches!(self.peek(), Token::For) {
25936                            self.advance();
25937                            args.push(self.parse_expr(0)?); // length
25938                        }
25939                        if !matches!(self.peek(), Token::RParen) {
25940                            return Err(self.err(format!(
25941                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25942                                self.peek()
25943                            )));
25944                        }
25945                        self.advance();
25946                        return Ok(Expr::FunctionCall {
25947                            name: String::from("overlay"),
25948                            args,
25949                        });
25950                    }
25951                    // `TRIM(chars FROM str)` — the keyword-less
25952                    // spelling lands here after the chars parse
25953                    // (the keyword forms return earlier).
25954                    if first.eq_ignore_ascii_case("trim")
25955                        && args.len() == 1
25956                        && matches!(self.peek(), Token::From)
25957                    {
25958                        self.advance();
25959                        let target = self.parse_expr(0)?;
25960                        if !matches!(self.peek(), Token::RParen) {
25961                            return Err(self.err(format!(
25962                                "expected ')' to close TRIM(chars FROM str), got {:?}",
25963                                self.peek()
25964                            )));
25965                        }
25966                        self.advance();
25967                        let chars = args.pop().expect("one arg");
25968                        return Ok(Expr::FunctionCall {
25969                            name: String::from("btrim"),
25970                            args: alloc::vec![target, chars],
25971                        });
25972                    }
25973                    // v7.24 (round-16 A) — aggregate-internal
25974                    // ordering: `array_agg(x ORDER BY y DESC NULLS
25975                    // LAST)`. Keys close the argument list.
25976                    if matches!(self.peek(), Token::Order) {
25977                        self.advance();
25978                        if !self.peek_is_by() {
25979                            return Err(self.err(format!(
25980                                "expected BY after ORDER in aggregate args, got {:?}",
25981                                self.peek()
25982                            )));
25983                        }
25984                        self.advance();
25985                        loop {
25986                            // v7.39 (round 691) — save/restore, the discipline this parser
25987                            // already uses around `pending_sample_preds`, so a subquery inside
25988                            // a key neither inherits nor leaks the channel.
25989                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25990                            let saved_coll = self.order_key_collation.take();
25991                            let parsed = self.parse_expr(0);
25992                            self.in_order_by_key = saved_flag;
25993                            let collation =
25994                                core::mem::replace(&mut self.order_key_collation, saved_coll);
25995                            let expr = parsed?;
25996                            let desc = if matches!(self.peek(), Token::Desc) {
25997                                self.advance();
25998                                true
25999                            } else if matches!(self.peek(), Token::Asc) {
26000                                self.advance();
26001                                false
26002                            } else {
26003                                false
26004                            };
26005                            let nulls_first = self.parse_optional_nulls_placement()?;
26006                            agg_order_by.push(OrderBy {
26007                                expr,
26008                                desc,
26009                                nulls_first,
26010                                collation,
26011                            });
26012                            if matches!(self.peek(), Token::Comma) {
26013                                self.advance();
26014                            } else {
26015                                break;
26016                            }
26017                        }
26018                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26019                        // follow the ORDER BY inside GROUP_CONCAT.
26020                        if self.consume_group_concat_separator(&mut args)? {
26021                            saw_separator = true;
26022                        }
26023                        if !matches!(self.peek(), Token::RParen) {
26024                            return Err(self.err(format!(
26025                                "expected ')' after aggregate ORDER BY, got {:?}",
26026                                self.peek()
26027                            )));
26028                        }
26029                        break;
26030                    }
26031                    // v7.39 (round 354, M12) — …or directly after the
26032                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26033                    // own spelling of what PG passes as string_agg's second
26034                    // argument; it was a parse error, so every MySQL query
26035                    // that names its own separator failed outright.
26036                    if self.consume_group_concat_separator(&mut args)? {
26037                        saw_separator = true;
26038                        break;
26039                    }
26040                    match self.peek() {
26041                        Token::Comma => {
26042                            self.advance();
26043                        }
26044                        Token::RParen => break,
26045                        other => {
26046                            return Err(self.err(format!(
26047                                "expected ',' or ')' in function args, got {other:?}"
26048                            )));
26049                        }
26050                    }
26051                }
26052            }
26053            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26054            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26055            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26056            // meaning a separator — that is what the explicit SEPARATOR
26057            // tail is for. Fold them into one `concat(...)` so the
26058            // aggregate keeps its single value argument.
26059            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26060                let values = args.len() - usize::from(saw_separator);
26061                if values > 1 {
26062                    let sep_arg = if saw_separator { args.pop() } else { None };
26063                    let folded = Expr::FunctionCall {
26064                        name: "concat".to_string(),
26065                        args: core::mem::take(&mut args),
26066                    };
26067                    args.push(folded);
26068                    if let Some(sep) = sep_arg {
26069                        args.push(sep);
26070                    }
26071                }
26072            }
26073            self.advance(); // consume ')'
26074            // v7.39 (read01 round 77) — named arguments are NOT reordered here
26075            // any more. The parser has no catalog, so it could only ever resolve
26076            // the handful of `make_*` builtins whose parameter names were baked
26077            // into a table right here — every user function got
26078            // "does not support named arguments", though the catalog has been
26079            // storing its parameter names all along. Reordering happens in eval,
26080            // in one place, for builtins and user functions alike.
26081            // v7.32 (round-29) — ordered-set aggregate tail
26082            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26083            // (percentile_cont / percentile_disc / mode). The sort spec
26084            // lands in the same `order_by` slot a decorated aggregate
26085            // uses; the executor dispatches on the function name. WITHIN
26086            // GROUP and an intra-argument ORDER BY are mutually
26087            // exclusive (PG rejects both).
26088            let within_group_order = self.parse_within_group_clause()?;
26089            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26090                return Err(self.err(
26091                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26092                        .into(),
26093                ));
26094            }
26095            let within_group_seen = !within_group_order.is_empty();
26096            let agg_order_by = if within_group_order.is_empty() {
26097                agg_order_by
26098            } else {
26099                within_group_order
26100            };
26101            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26102            let filter = self.parse_filter_clause()?;
26103            // v4.12: window-function tail — `name(args) OVER (...)`.
26104            // Promotes the just-parsed FunctionCall into a
26105            // WindowFunction node carrying partition + order.
26106            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26107            // / `RESPECT NULLS OVER (...)` between the closing paren
26108            // and `OVER`.
26109            let null_treatment = self.parse_null_treatment_modifier();
26110            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26111                && s.eq_ignore_ascii_case("over")
26112            {
26113                self.advance();
26114                // v7.39 (round 230) — PG implements neither modifier for a
26115                // windowed call and says so (0A000). Both used to be parsed
26116                // and then silently dropped here, so `count(DISTINCT v)
26117                // OVER (…)` quietly answered the non-distinct count.
26118                if agg_distinct {
26119                    return Err(
26120                        self.err("DISTINCT is not implemented for window functions".to_string())
26121                    );
26122                }
26123                if !agg_order_by.is_empty() {
26124                    // PG separates the two shapes that land here: a
26125                    // WITHIN GROUP call is an ordered-set aggregate and gets
26126                    // its own message naming the aggregate; a plain
26127                    // `agg(x ORDER BY y)` gets the generic one.
26128                    let msg = if within_group_seen {
26129                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26130                    } else {
26131                        "aggregate ORDER BY is not implemented for window functions".to_string()
26132                    };
26133                    return Err(self.err(msg));
26134                }
26135                let (partition_by, order_by, frame) = self.parse_over_clause()?;
26136                return Ok(Expr::WindowFunction {
26137                    name: first,
26138                    args,
26139                    partition_by,
26140                    order_by,
26141                    frame,
26142                    null_treatment,
26143                    filter,
26144                });
26145            }
26146            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26147                return Ok(Expr::AggregateOrdered {
26148                    call: Box::new(Expr::FunctionCall { name: first, args }),
26149                    order_by: agg_order_by,
26150                    distinct: agg_distinct,
26151                    filter,
26152                });
26153            }
26154            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26155            // over TIMESTAMPTZ and has no timestamp overload, so a
26156            // timestamp argument is coerced on the way in and the answer
26157            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26158            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26159            // zone`. SPG answered `timestamp without time zone`, dropping
26160            // the offset from every rendering.
26161            //
26162            // Writing the coercion PG performs makes the existing
26163            // argument-driven typing (the one `date_trunc` uses) reach the
26164            // right answer, rather than teaching the type layer a second
26165            // rule. MySQL's DATE_ADD is a different function that returns
26166            // DATE or DATETIME, so this is PG-dialect only.
26167            //
26168            // Out-of-line because this sits on the RECURSIVE descent
26169            // frame: an inline block with locals here costs every nesting
26170            // level, and the suite's deep-nesting sentinel overflowed the
26171            // 512 KiB parser stack the moment one was added (round 430's
26172            // lesson, in the same shape).
26173            if !self.mysql_dialect {
26174                lift_date_add_arg_to_timestamptz(&first, &mut args);
26175            }
26176            return Ok(Expr::FunctionCall { name: first, args });
26177        }
26178        // v7.9.20 — SQL-standard parenless keyword expressions
26179        // (PG treats these as functions called without parens).
26180        // Resolve to a synthetic FunctionCall so the engine's
26181        // eval path reuses the existing function-call routing.
26182        // mailrs G3.
26183        let lc = first.to_ascii_lowercase();
26184        if matches!(
26185            lc.as_str(),
26186            "current_date"
26187                | "current_time"
26188                | "current_timestamp"
26189                | "localtimestamp"
26190                | "localtime"
26191                // v7.37.17 (17.6 siblings) — session-identity SQL-
26192                // standard parenless keywords. current_user /
26193                // session_user / user were already caught by the
26194                // pgwire canned-response shortcut but bare-select
26195                // in the embedded engine went through Expr::Column
26196                // and errored. Adding them here so the parser
26197                // resolves to a synthetic FunctionCall that reuses
26198                // the existing eval/functions.rs dispatch.
26199                | "current_user"
26200                | "session_user"
26201                | "current_role"
26202                | "current_catalog"
26203                | "current_schema"
26204                | "current_database"
26205                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26206                | "system_user"
26207        ) {
26208            return Ok(Expr::FunctionCall {
26209                name: lc,
26210                args: Vec::new(),
26211            });
26212        }
26213        Ok(Expr::Column(ColumnName {
26214            qualifier: None,
26215            name: first,
26216        }))
26217    }
26218}
26219
26220/// v7.39 (round 522) — write the coercion PG's `date_add` /
26221/// `date_subtract` signature performs.
26222///
26223/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26224/// timestamp argument is cast on the way in and the answer is
26225/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26226/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26227/// `timestamp without time zone`, dropping the offset from every
26228/// rendering of the result.
26229///
26230/// Writing the cast the signature implies lets the existing
26231/// argument-driven typing (the one `date_trunc` uses) reach the right
26232/// answer instead of teaching the type layer a second rule. MySQL's
26233/// DATE_ADD is a different function returning DATE or DATETIME, so the
26234/// caller applies this in PG dialect only.
26235///
26236/// A free function, and not a block at the call site, because the caller
26237/// is on the recursive-descent frame chain.
26238#[inline(never)]
26239fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26240    if args.len() != 2
26241        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26242    {
26243        return;
26244    }
26245    let base = args.remove(0);
26246    args.insert(
26247        0,
26248        Expr::Cast {
26249            expr: Box::new(base),
26250            target: CastTarget::Timestamptz,
26251        },
26252    );
26253}
26254
26255/// v6.8.2 — walk an expression tree and return the first column
26256/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26257/// to derive `CreateIndexStatement.column` from an expression
26258/// key (so downstream planner code resolving a primary column
26259/// position keeps working with expression indexes). Returns
26260/// `None` when the expression has no column ref at all — caller
26261/// surfaces that as a parse error.
26262fn extract_first_column(expr: &Expr) -> Option<String> {
26263    match expr {
26264        Expr::Column(cn) => Some(cn.name.clone()),
26265        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26266        Expr::Binary { lhs, rhs, .. } => {
26267            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26268        }
26269        Expr::Unary { expr: e, .. } => extract_first_column(e),
26270        // v7.39 (read01 round 93) — a cast wraps its operand: a common
26271        // expression-index key is `lower(col::text)`, where the column
26272        // sits under the `::text` cast inside the function arg. Without
26273        // descending here the key was rejected as "references no column".
26274        Expr::Cast { expr: e, .. } => extract_first_column(e),
26275        _ => None,
26276    }
26277}
26278
26279fn maybe_not(expr: Expr, negated: bool) -> Expr {
26280    if negated {
26281        Expr::Unary {
26282            op: UnOp::Not,
26283            expr: Box::new(expr),
26284        }
26285    } else {
26286        expr
26287    }
26288}
26289
26290/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26291/// things in the two dialects, and SPG read all three PG's way:
26292///
26293/// | token | PG (and SPG) | MySQL, measured |
26294/// |---|---|---|
26295/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26296/// | `&&` | inet / array overlap | **AND** |
26297/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26298///
26299/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26300/// answer with no error, which is why they are routed here rather than
26301/// left to the shared table.
26302impl Parser {
26303    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26304        if self.mysql_dialect {
26305            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26306            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26307            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26308            if let Token::Ident(w) = tok
26309                && w.eq_ignore_ascii_case("div")
26310            {
26311                return Some((BinOp::IntDiv, 8));
26312            }
26313            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26314            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26315            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26316            // there sits in operand position, not infix).
26317            if let Token::Ident(w) = tok
26318                && w.eq_ignore_ascii_case("mod")
26319            {
26320                return Some((BinOp::Mod, 8));
26321            }
26322            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26323            // plain ident to the lexer. Its precedence sits between OR (1)
26324            // and AND (3) — hence rung 2, the slot freed by moving AND up.
26325            if let Token::Ident(w) = tok
26326                && w.eq_ignore_ascii_case("xor")
26327            {
26328                return Some((BinOp::LogicalXor, 2));
26329            }
26330            match tok {
26331                Token::Concat => return Some((BinOp::Or, 1)),
26332                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26333                Token::InetOverlap => return Some((BinOp::And, 3)),
26334                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26335                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26336                _ => {}
26337            }
26338        }
26339        binop_from(tok)
26340    }
26341}
26342
26343// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26344// (which sits strictly between OR and AND), every level from AND upward was
26345// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26346// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26347// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26348// the *relative* order of every PG operator is unchanged by the shift.
26349fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26350    let pair = match tok {
26351        Token::Or => (BinOp::Or, 1),
26352        Token::And => (BinOp::And, 3),
26353        Token::Eq => (BinOp::Eq, 5),
26354        Token::NotEq => (BinOp::NotEq, 5),
26355        Token::Lt => (BinOp::Lt, 5),
26356        Token::LtEq => (BinOp::LtEq, 5),
26357        Token::Gt => (BinOp::Gt, 5),
26358        Token::GtEq => (BinOp::GtEq, 5),
26359        // pgvector distance ops all sit on the same rung — tighter than
26360        // comparisons (5) so `col <-> v < threshold` parses correctly.
26361        Token::L2Distance => (BinOp::L2Distance, 6),
26362        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26363        // comparison rung.
26364        Token::GeomParallel => (BinOp::GeomParallel, 5),
26365        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26366        // comparison rung.
26367        Token::OverLeft => (BinOp::OverLeft, 5),
26368        Token::OverRight => (BinOp::OverRight, 5),
26369        Token::GeomPerp => (BinOp::GeomPerp, 5),
26370        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26371        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26372        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26373        Token::InnerProduct => (BinOp::InnerProduct, 6),
26374        Token::CosineDistance => (BinOp::CosineDistance, 6),
26375        Token::Plus => (BinOp::Add, 7),
26376        Token::Minus => (BinOp::Sub, 7),
26377        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26378        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26379        // pgvector distances above) BETWEEN additive (7) and the
26380        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26381        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26382        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26383        // ("matches PG conceptually" — the round-753 audit measured it
26384        // false; the old rung errored on `'a' || 1 + 1` with
26385        // `text + integer`). Same-level chains left-fold, as PG does.
26386        Token::Concat => (BinOp::Concat, 6),
26387        Token::Pipe => (BinOp::BitOr, 6),
26388        Token::Amp => (BinOp::BitAnd, 6),
26389        Token::Star => (BinOp::Mul, 8),
26390        Token::Slash => (BinOp::Div, 8),
26391        Token::Percent => (BinOp::Mod, 8),
26392        // v4.14: JSON path ops bind tighter than comparisons (5)
26393        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26394        // Same rung as the multiplicative ops.
26395        Token::JsonGet => (BinOp::JsonGet, 8),
26396        Token::JsonGetText => (BinOp::JsonGetText, 8),
26397        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26398        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26399        Token::JsonContains => (BinOp::JsonContains, 8),
26400        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26401        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26402        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26403        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26404        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26405        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26406        // v7.12.2 — `@@` binds at the comparison rung (looser than
26407        // arithmetic, tighter than AND / OR). PG places `@@` at
26408        // the same precedence as `=` / `<`, so we follow.
26409        Token::TsMatch => (BinOp::TsMatch, 5),
26410        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26411        // PG places these at the comparison rung (same level as `=`),
26412        // so we follow.
26413        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26414        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26415        Token::InetContains => (BinOp::InetContains, 5),
26416        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26417        Token::InetOverlap => (BinOp::InetOverlap, 5),
26418        // v7.39 (round 508) — the geometric and pattern-order predicates
26419        // ride the comparison rung, as every other predicate does.
26420        Token::Intersects => (BinOp::Intersects, 5),
26421        Token::IsBelow => (BinOp::IsBelow, 5),
26422        Token::IsAbove => (BinOp::IsAbove, 5),
26423        Token::PatternLt => (BinOp::PatternLt, 5),
26424        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26425        Token::PatternGt => (BinOp::PatternGt, 5),
26426        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26427        // `@@@` is the old spelling of `@@` and means exactly it.
26428        Token::TsMatchOld => (BinOp::TsMatch, 5),
26429        _ => return None,
26430    };
26431    Some(pair)
26432}
26433
26434#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26435// `as f32` here is intentional: vector elements widen / narrow into f32 on
26436// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26437// past ~15 decimal digits — both are acceptable for a fixed-precision
26438// pgvector column.
26439/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26440/// implicit table alias and break trailing clauses. WITH lands
26441/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26442/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26443/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26444/// / VALUES / FOR / LATERAL — all of which would otherwise be
26445/// silently swallowed by `parse_optional_alias`.
26446fn is_alias_stopword(s: &str) -> bool {
26447    matches!(
26448        s.to_ascii_lowercase().as_str(),
26449        "with"
26450            | "on"
26451            | "where"
26452            | "having"
26453            | "group"
26454            | "order"
26455            | "limit"
26456            | "offset"
26457            | "union"
26458            | "except"
26459            | "intersect"
26460            | "returning"
26461            | "set"
26462            | "values"
26463            | "for"
26464            | "window"
26465            | "tablesample"
26466            | "lateral"
26467            | "left"
26468            | "right"
26469            | "inner"
26470            | "outer"
26471            | "full"
26472            | "cross"
26473            | "join"
26474            | "natural"
26475            | "using"
26476            | "fetch"
26477    )
26478}
26479
26480fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26481    match e {
26482        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26483        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26484        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26485        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26486        // so scale the divisor by hand instead of `f32::powi`.)
26487        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26488            let mut div = 1.0f32;
26489            for _ in 0..*scale {
26490                div *= 10.0;
26491            }
26492            Some(*unscaled as f32 / div)
26493        }
26494        Expr::Unary {
26495            op: UnOp::Neg,
26496            expr,
26497        } => extract_numeric_literal(expr).map(|x| -x),
26498        _ => None,
26499    }
26500}
26501
26502/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26503/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26504/// negative. Returns `None` if any pair fails to parse or no pair is found.
26505///
26506/// Recognised units (case-insensitive, optional trailing `s`):
26507/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26508/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26509/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26510/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26511/// (PG-canonical: DST and month-boundary semantics depend on this).
26512/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26513/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26514/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26515#[allow(clippy::cast_possible_truncation)]
26516fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26517    let mut months: i64 = 0;
26518    let mut days: i64 = 0;
26519    let mut micros: i64 = 0;
26520    let mut in_time = false;
26521    let mut num = alloc::string::String::new();
26522    for ch in rest.chars() {
26523        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26524            num.push(ch);
26525            continue;
26526        }
26527        if ch == 'T' || ch == 't' {
26528            if !num.is_empty() {
26529                return None;
26530            }
26531            in_time = true;
26532            continue;
26533        }
26534        let n: f64 = num.parse().ok()?;
26535        num.clear();
26536        match (ch, in_time) {
26537            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26538            ('M', false) => months += n as i64,
26539            ('W' | 'w', false) => days += (n * 7.0) as i64,
26540            ('D' | 'd', false) => days += n as i64,
26541            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26542            ('M', true) => micros += (n * 60_000_000.0) as i64,
26543            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26544            _ => return None,
26545        }
26546    }
26547    if !num.is_empty() {
26548        return None;
26549    }
26550    Some((
26551        i32::try_from(months).ok()?,
26552        i32::try_from(days).ok()?,
26553        micros,
26554    ))
26555}
26556
26557/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26558/// leading `-` negates the whole value). Rejects date-like strings.
26559fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26560    let (neg, body) = match s.strip_prefix('-') {
26561        Some(b) => (true, b),
26562        None => (false, s),
26563    };
26564    let (y, m) = body.split_once('-')?;
26565    let years: i32 = y.parse().ok()?;
26566    let mons: i32 = m.parse().ok()?;
26567    if years < 0 || mons < 0 {
26568        return None;
26569    }
26570    let total = years.checked_mul(12)?.checked_add(mons)?;
26571    Some((if neg { -total } else { total }, 0, 0))
26572}
26573
26574/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26575/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26576fn parse_interval_clock(tok: &str) -> Option<i64> {
26577    let (neg, body) = match tok.strip_prefix('-') {
26578        Some(r) => (true, r),
26579        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26580    };
26581    let mut it = body.split(':');
26582    let h: i64 = it.next()?.parse().ok()?;
26583    let m: i64 = it.next()?.parse().ok()?;
26584    let s_tok = it.next().unwrap_or("0");
26585    if it.next().is_some() {
26586        return None;
26587    }
26588    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26589        let sec: i64 = sec.parse().ok()?;
26590        let mut f = alloc::string::String::from(frac);
26591        while f.len() < 6 {
26592            f.push('0');
26593        }
26594        f.truncate(6);
26595        let fus: i64 = f.parse().ok()?;
26596        sec.checked_mul(1_000_000)?.checked_add(fus)?
26597    } else {
26598        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26599    };
26600    let total = h
26601        .checked_mul(3_600_000_000)?
26602        .checked_add(m.checked_mul(60_000_000)?)?
26603        .checked_add(sec_us)?;
26604    Some(if neg { -total } else { total })
26605}
26606
26607/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26608/// every spelling PG accepts (measured against live PG18.4, not guessed):
26609/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26610/// Before this, the unit table matched long names only, with an ad-hoc
26611/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26612/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26613/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26614/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26615/// fractional) both read from this one table now.
26616fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26617    let u = raw.to_ascii_lowercase();
26618    Some(match u.as_str() {
26619        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26620            "microsecond"
26621        }
26622        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26623            "millisecond"
26624        }
26625        "second" | "seconds" | "sec" | "secs" | "s" => "second",
26626        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26627        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26628        "day" | "days" | "d" => "day",
26629        "week" | "weeks" | "w" => "week",
26630        "month" | "months" | "mon" | "mons" => "month",
26631        "year" | "years" | "yr" | "yrs" | "y" => "year",
26632        "decade" | "decades" | "dec" | "decs" => "decade",
26633        "century" | "centuries" | "cent" | "c" => "century",
26634        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26635        _ => return None,
26636    })
26637}
26638
26639/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26640/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26641#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26642pub(crate) enum IntervalField {
26643    Year,
26644    Month,
26645    Day,
26646    Hour,
26647    Minute,
26648    Second,
26649}
26650
26651/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26652/// spellings aren't standard for the qualifier position, so only the singular
26653/// forms are accepted.
26654/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26655/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26656/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26657/// take a `'1 2'` style literal — are not read here; they stay a parse
26658/// error rather than being silently misread.)
26659/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26660///
26661/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26662/// to do with a `@@` engine setting, and an unset one reads NULL rather
26663/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26664/// were the same node and `SELECT @x` answered "Unknown system variable".)
26665/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26666/// not see a session override — measured, after `SET autocommit=0`,
26667/// `@@global.autocommit` is still 1.
26668///
26669/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26670/// the parser's nesting budget is tuned against, and building these
26671/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26672/// wall `parse_left_right_atom` and friends were factored out for).
26673#[inline(never)]
26674fn variable_ref_atom(raw: &str) -> Expr {
26675    let user_var = !raw.starts_with("@@");
26676    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26677    Expr::FunctionCall {
26678        name: String::from(if user_var {
26679            "__spg_user_var"
26680        } else {
26681            "__spg_session_var"
26682        }),
26683        args: alloc::vec![Expr::Literal(Literal::String(bare))],
26684    }
26685}
26686
26687fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26688    let Token::Ident(s) = tok else { return None };
26689    Some(match () {
26690        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26691        () if s.eq_ignore_ascii_case("second") => "second",
26692        () if s.eq_ignore_ascii_case("minute") => "minute",
26693        () if s.eq_ignore_ascii_case("hour") => "hour",
26694        () if s.eq_ignore_ascii_case("day") => "day",
26695        () if s.eq_ignore_ascii_case("week") => "week",
26696        () if s.eq_ignore_ascii_case("month") => "month",
26697        () if s.eq_ignore_ascii_case("quarter") => "quarter",
26698        () if s.eq_ignore_ascii_case("year") => "year",
26699        () => return None,
26700    })
26701}
26702
26703/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26704/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26705/// which constructs the value at run time. Only the slot the unit names
26706/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26707/// slot the builtin has (months and fractional seconds respectively).
26708fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26709    let zero = || Expr::Literal(Literal::Integer(0));
26710    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26711        lhs: alloc::boxed::Box::new(qty.clone()),
26712        op,
26713        rhs: alloc::boxed::Box::new(by),
26714    };
26715    // (years, months, weeks, days, hours, mins, secs)
26716    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26717    match unit {
26718        "year" => args[0] = qty,
26719        "quarter" => {
26720            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26721        }
26722        "month" => args[1] = qty,
26723        "week" => args[2] = qty,
26724        "day" => args[3] = qty,
26725        "hour" => args[4] = qty,
26726        "minute" => args[5] = qty,
26727        "second" => args[6] = qty,
26728        // The builtin's seconds slot takes a fraction, so microseconds ride
26729        // it scaled down; the divisor is a NUMERIC literal so the division
26730        // stays exact rather than going through a float.
26731        "microsecond" => {
26732            args[6] = scaled(
26733                crate::ast::BinOp::Div,
26734                Expr::Literal(Literal::Numeric {
26735                    unscaled: 1_000_000,
26736                    scale: 0,
26737                }),
26738            );
26739        }
26740        _ => args[3] = qty,
26741    }
26742    Expr::FunctionCall {
26743        name: alloc::string::String::from("make_interval"),
26744        args,
26745    }
26746}
26747
26748/// `(count, unit)` → `(months, days, micros)`.
26749fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26750    let n: i64 = count.trim().parse().ok()?;
26751    Some(match unit {
26752        "microsecond" => (0, 0, n),
26753        "second" => (0, 0, n.checked_mul(1_000_000)?),
26754        "minute" => (0, 0, n.checked_mul(60_000_000)?),
26755        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26756        "day" => (0, i32::try_from(n).ok()?, 0),
26757        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26758        "month" => (i32::try_from(n).ok()?, 0, 0),
26759        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26760        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26761        _ => return None,
26762    })
26763}
26764
26765fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26766    let Token::Ident(s) = tok else { return None };
26767    Some(match () {
26768        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26769        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26770        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26771        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26772        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26773        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26774        () => return None,
26775    })
26776}
26777
26778/// v7.39 (read01 round 102) — interpret an interval literal under a field
26779/// qualifier. Returns `(months, days, micros)`.
26780///
26781/// * A single field applied to a bare number sets which unit the number means,
26782///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26783///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26784/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26785/// * Every other range, and any literal a single field can't read as a plain
26786///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26787///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26788///   like PG, and the qualifier there only bounds precision.
26789fn interpret_qualified_interval(
26790    text: &str,
26791    (f1, f2): (IntervalField, Option<IntervalField>),
26792) -> Option<(i32, i32, i64)> {
26793    if let Some(f2) = f2 {
26794        if f1 == IntervalField::Year && f2 == IntervalField::Month {
26795            if let Some(m) = parse_year_month_literal(text) {
26796                return Some((m, 0, 0));
26797            }
26798        }
26799        return parse_interval_text(text);
26800    }
26801    // Single field: reinterpret a bare number; otherwise the default parse.
26802    let trimmed = text.trim();
26803    if let Ok(val) = trimmed.parse::<f64>() {
26804        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26805        #[allow(clippy::cast_possible_truncation)]
26806        let whole = val as i64;
26807        #[allow(clippy::cast_possible_truncation)]
26808        let secs_micros = {
26809            let m = val * 1_000_000.0;
26810            if m >= 0.0 {
26811                (m + 0.5) as i64
26812            } else {
26813                (m - 0.5) as i64
26814            }
26815        };
26816        return Some(match f1 {
26817            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26818            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26819            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26820            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26821            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26822            IntervalField::Second => (0, 0, secs_micros),
26823        });
26824    }
26825    parse_interval_text(text)
26826}
26827
26828/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26829fn parse_year_month_literal(text: &str) -> Option<i32> {
26830    let t = text.trim();
26831    let (neg, body) = match t.strip_prefix('-') {
26832        Some(r) => (true, r),
26833        None => (false, t.strip_prefix('+').unwrap_or(t)),
26834    };
26835    let mut it = body.split('-');
26836    let years: i32 = it.next()?.trim().parse().ok()?;
26837    let months: i32 = match it.next() {
26838        Some(m) => m.trim().parse().ok()?,
26839        None => 0,
26840    };
26841    if it.next().is_some() {
26842        return None;
26843    }
26844    let total = years.checked_mul(12)?.checked_add(months)?;
26845    Some(if neg { -total } else { total })
26846}
26847
26848pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26849    // v7.38.19 — the two infinities, answered as the three extreme
26850    // fields PostgreSQL itself puts on the wire for them:
26851    //
26852    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
26853    //     … 7fffffffffffffff 7fffffff 7fffffff
26854    //
26855    // So no caller has to know the spelling — every one of them already
26856    // reads the three numbers, and `IntervalKind::from_fields` names
26857    // what they mean.
26858    //
26859    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
26860    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
26861    // infinity. Interval takes the full word, in any case.
26862    {
26863        let word = s.trim();
26864        let word = word.strip_prefix('@').map_or(word, str::trim);
26865        let (neg, body) = match word.strip_prefix('-') {
26866            Some(rest) => (true, rest.trim_start()),
26867            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
26868        };
26869        if body.eq_ignore_ascii_case("infinity") {
26870            return Some(if neg {
26871                (i32::MIN, i32::MIN, i64::MIN)
26872            } else {
26873                (i32::MAX, i32::MAX, i64::MAX)
26874            });
26875        }
26876    }
26877    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26878    // `@` is decorative; a trailing `ago` negates the whole interval.
26879    let mut trimmed = s.trim();
26880    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26881    let mut negate = false;
26882    if let Some(rest) = trimmed
26883        .strip_suffix("ago")
26884        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26885    {
26886        negate = true;
26887        trimmed = rest.trim();
26888    }
26889    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26890        let (mo, d, us) = v?;
26891        if negate {
26892            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26893        } else {
26894            Some((mo, d, us))
26895        }
26896    };
26897    let s = trimmed;
26898    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26899    // are single tokens, not the `<n> <unit>` pair form handled below.
26900    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26901        return finish(parse_iso8601_interval(rest));
26902    }
26903    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26904        if let Some(iv) = parse_year_month_interval(trimmed) {
26905            return finish(Some(iv));
26906        }
26907    }
26908    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26909    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26910    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26911    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26912        if let Ok(n) = trimmed.parse::<i64>() {
26913            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26914        }
26915        if let Ok(f) = trimmed.parse::<f64>() {
26916            if f.is_finite() {
26917                #[allow(clippy::cast_possible_truncation)]
26918                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26919            }
26920        }
26921    }
26922    // v7.39 (round 243) — PG accepts the number and unit run together
26923    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26924    // the `<n> <unit>` pair loop below sees them as two.
26925    let raw_parts: Vec<&str> = s.split_whitespace().collect();
26926    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26927    for p in raw_parts {
26928        let boundary = p
26929            .char_indices()
26930            .find(|(i, c)| {
26931                *i > 0
26932                    && c.is_ascii_alphabetic()
26933                    && p[..*i]
26934                        .chars()
26935                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26936                    && p[..*i].chars().any(|d| d.is_ascii_digit())
26937            })
26938            .map(|(i, _)| i);
26939        match boundary {
26940            Some(i) => {
26941                parts.push(&p[..i]);
26942                parts.push(&p[i..]);
26943            }
26944            None => parts.push(p),
26945        }
26946    }
26947    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26948    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26949    // remains is the `<n> <unit>` pair form handled below.
26950    let mut clock_us: i64 = 0;
26951    let mut had_clock = false;
26952    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26953        clock_us = parse_interval_clock(parts[pos])?;
26954        parts.remove(pos);
26955        had_clock = true;
26956    }
26957    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26958    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26959    let mut lone_days: i32 = 0;
26960    if had_clock && parts.len() == 1 {
26961        if let Ok(n) = parts[0].parse::<i64>() {
26962            lone_days = i32::try_from(n).ok()?;
26963            parts.clear();
26964        }
26965    }
26966    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26967        return None;
26968    }
26969    let mut months: i32 = 0;
26970    let mut days: i32 = lone_days;
26971    let mut micros: i64 = clock_us;
26972    let mut i = 0;
26973    while i < parts.len() {
26974        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26975        if let Ok(n) = parts[i].parse::<i64>() {
26976            match unit_stripped {
26977                "microsecond" => micros = micros.checked_add(n)?,
26978                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26979                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26980                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26981                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26982                "day" => {
26983                    let n32 = i32::try_from(n).ok()?;
26984                    days = days.checked_add(n32)?;
26985                }
26986                "week" => {
26987                    let n32 = i32::try_from(n).ok()?;
26988                    days = days.checked_add(n32.checked_mul(7)?)?;
26989                }
26990                "month" => {
26991                    let n32 = i32::try_from(n).ok()?;
26992                    months = months.checked_add(n32)?;
26993                }
26994                "year" => {
26995                    let n32 = i32::try_from(n).ok()?;
26996                    months = months.checked_add(n32.checked_mul(12)?)?;
26997                }
26998                // v7.39 (read01 timestamp.c) — the larger calendar units.
26999                "decade" => {
27000                    let n32 = i32::try_from(n).ok()?;
27001                    months = months.checked_add(n32.checked_mul(120)?)?;
27002                }
27003                "century" => {
27004                    let n32 = i32::try_from(n).ok()?;
27005                    months = months.checked_add(n32.checked_mul(1200)?)?;
27006                }
27007                "millennium" => {
27008                    let n32 = i32::try_from(n).ok()?;
27009                    months = months.checked_add(n32.checked_mul(12000)?)?;
27010                }
27011                _ => return None,
27012            }
27013        } else if let Ok(f) = parts[i].parse::<f64>() {
27014            // Fractional units cascade down to the next-finer field the way
27015            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27016            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27017            // no_std: f64 has no trunc/fract/round methods, so do them with
27018            // casts (toward-zero) + explicit round-half-away-from-zero.
27019            #[allow(clippy::cast_possible_truncation)]
27020            fn round_i64(x: f64) -> i64 {
27021                if x >= 0.0 {
27022                    (x + 0.5) as i64
27023                } else {
27024                    (x - 0.5) as i64
27025                }
27026            }
27027            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27028            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27029                const DAY_US: f64 = 86_400_000_000.0;
27030                let whole = d as i64; // truncates toward zero
27031                let frac = d - whole as f64;
27032                *days = days.checked_add(i32::try_from(whole).ok()?)?;
27033                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27034                Some(())
27035            }
27036            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27037            match unit_stripped {
27038                "microsecond" => micros = micros.checked_add(round_i64(f))?,
27039                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27040                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27041                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27042                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27043                "day" => add_days_frac(&mut days, &mut micros, f)?,
27044                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27045                "month" => {
27046                    let whole = f as i64;
27047                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27048                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27049                }
27050                "year" => {
27051                    let m = f * 12.0;
27052                    let whole = m as i64;
27053                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27054                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27055                }
27056                _ => return None,
27057            }
27058        } else {
27059            return None;
27060        }
27061        i += 2;
27062    }
27063    finish(Some((months, days, micros)))
27064}
27065
27066/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27067/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27068/// `interval` is intentionally absent (handled by its own parser arm).
27069/// Returns `None` for names that aren't sensible as a bare typed literal, so
27070/// the caller falls back to treating the ident as a column reference.
27071fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27072    Some(match ident {
27073        "date" => CastTarget::Date,
27074        "timestamp" | "datetime" => CastTarget::Timestamp,
27075        "timestamptz" => CastTarget::Timestamptz,
27076        "bool" | "boolean" => CastTarget::Bool,
27077        "int" | "integer" | "int4" => CastTarget::Int,
27078        "bigint" | "int8" => CastTarget::BigInt,
27079        "float8" | "double precision" => CastTarget::Float,
27080        "uuid" => CastTarget::Uuid,
27081        "bytea" => CastTarget::Bytea,
27082        "json" => CastTarget::Json,
27083        "jsonb" => CastTarget::Jsonb,
27084        // Types without a dedicated CastTarget variant flow through the
27085        // generic Named path (engine resolves via column_type_to_data_type).
27086        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27087        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27088        | "money" | "bit" | "varbit"
27089        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27090        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27091        // Range / multirange types likewise.
27092        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27093        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27094        | "datemultirange" | "tsmultirange" | "tstzmultirange"
27095        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27096        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27097            CastTarget::Named(alloc::string::String::from(ident))
27098        }
27099        _ => return None,
27100    })
27101}
27102
27103/// v7.12.4 — map a bare type-name identifier (the form that
27104/// appears in a function arg list or RETURNS clause) to a
27105/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27106/// types so the caller can preserve them as
27107/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27108///
27109/// Subset of the full column-type grammar — we deliberately
27110/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27111/// here because function-arg types in v7.12.4 are mostly the
27112/// bare form (`text`, `int`, `bytea`, …).
27113/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27114/// than being `name TYPE`?
27115///
27116/// The multi-word spellings SQL allows for a bare argument type, each
27117/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27118///
27119/// NOTE this list also exists in `spg-storage`, which computes the
27120/// signature key from the rendered argument text and has to reach the
27121/// same verdict. The two crates are siblings — neither depends on the
27122/// other — and each already carries its own table of type spellings
27123/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27124/// there), so this follows the structure rather than inventing new
27125/// duplication. Recorded as V49.
27126pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27127    let t = phrase.trim().to_ascii_lowercase();
27128    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27129    matches!(
27130        base,
27131        "double precision"
27132            | "character varying"
27133            | "bit varying"
27134            | "timestamp with time zone"
27135            | "timestamp without time zone"
27136            | "time with time zone"
27137            | "time without time zone"
27138            | "national character"
27139            | "national character varying"
27140    )
27141}
27142
27143fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27144    Some(match ident.to_ascii_lowercase().as_str() {
27145        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27146        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27147        "bigint" => ColumnTypeName::BigInt,
27148        "float" | "double" => ColumnTypeName::Float,
27149        // v7.39 (round 269) — real is 32-bit.
27150        "real" | "float4" => ColumnTypeName::Real,
27151        "text" => ColumnTypeName::Text,
27152        "bool" | "boolean" => ColumnTypeName::Bool,
27153        "date" => ColumnTypeName::Date,
27154        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27155        "timestamptz" => ColumnTypeName::Timestamptz,
27156        "json" => ColumnTypeName::Json,
27157        "jsonb" => ColumnTypeName::Jsonb,
27158        "bytea" | "bytes" => ColumnTypeName::Bytes,
27159        "tsvector" => ColumnTypeName::TsVector,
27160        "tsquery" => ColumnTypeName::TsQuery,
27161        "uuid" => ColumnTypeName::Uuid,
27162        "interval" => ColumnTypeName::Interval,
27163        "time" => ColumnTypeName::Time,
27164        "year" => ColumnTypeName::Year,
27165        "timetz" => ColumnTypeName::TimeTz,
27166        "money" => ColumnTypeName::Money,
27167        _ => return None,
27168    })
27169}
27170
27171/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27172/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27173///
27174/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27175/// / embedded SQL land in v7.12.5+):
27176///
27177/// ```text
27178///   body          := [ws] block [ws]
27179///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27180///   stmt          := assign | return
27181///   assign        := assign_target := expr
27182///   assign_target := ( NEW | OLD ) . ident | ident
27183///   return        := RETURN ( NEW | OLD | NULL | expr )
27184/// ```
27185///
27186/// `expr` is parsed by recursing into the regular `Parser` — so a
27187/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27188/// NEW.subject || ' ' || NEW.sender)` body shape works without
27189/// the body parser knowing what `to_tsvector` is.
27190///
27191/// Errors here cause the caller to fall back to
27192/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27193/// successful, but the executor will refuse to invoke the
27194/// function with an "unparseable body" error.
27195/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27196/// from the crate root as `spg_sql::parse_function_body`.
27197pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27198    parse_plpgsql_body(body)
27199}
27200
27201fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27202    // Use the regular lexer on the body text. The trailing
27203    // `END;` may or may not have a semicolon; the lexer treats
27204    // both forms identically.
27205    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27206        message: alloc::format!("plpgsql body lex error: {e}"),
27207        token_pos: 0,
27208    })?;
27209    let mut parser = Parser::new(tokens);
27210    parser.parse_plpgsql_block()
27211}
27212
27213/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27214fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27215    match v {
27216        crate::ast::SetValue::String(s)
27217        | crate::ast::SetValue::Ident(s)
27218        | crate::ast::SetValue::Number(s) => s.clone(),
27219        crate::ast::SetValue::Default => "DEFAULT".into(),
27220    }
27221}
27222
27223/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27224/// contains an aggregate call at ITS OWN query level (recursion stops at
27225/// sublink boundaries — a sublink's aggregates belong to the sublink).
27226/// Backs the "aggregate functions are not allowed in a recursive query's
27227/// recursive term" well-formedness check.
27228fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27229    const AGG_NAMES: &[&str] = &[
27230        "count",
27231        "sum",
27232        "min",
27233        "max",
27234        "avg",
27235        "string_agg",
27236        "array_agg",
27237        "bool_and",
27238        "bool_or",
27239        "every",
27240        "any_value",
27241        "json_agg",
27242        "jsonb_agg",
27243        "json_object_agg",
27244        "jsonb_object_agg",
27245        "bit_and",
27246        "bit_or",
27247        "bit_xor",
27248        "var_pop",
27249        "var_samp",
27250        "variance",
27251        "stddev",
27252        "stddev_pop",
27253        "stddev_samp",
27254        "range_agg",
27255        "range_intersect_agg",
27256        "percentile_cont",
27257        "percentile_disc",
27258        "mode",
27259        "corr",
27260        "covar_pop",
27261        "covar_samp",
27262    ];
27263    match e {
27264        Expr::AggregateOrdered { .. } => true,
27265        Expr::FunctionCall { name, args } => {
27266            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27267                || args.iter().any(expr_has_toplevel_aggregate)
27268        }
27269        Expr::NamedArg { expr, .. }
27270        | Expr::Variadic(expr)
27271        | Expr::Unary { expr, .. }
27272        | Expr::Cast { expr, .. }
27273        | Expr::IsNull { expr, .. }
27274        | Expr::FieldAccess { base: expr, .. }
27275        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27276        Expr::Binary { lhs, rhs, .. } => {
27277            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27278        }
27279        Expr::Like { expr, pattern, .. } => {
27280            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27281        }
27282        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27283        Expr::InList { expr, list, .. } => {
27284            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27285        }
27286        Expr::ArraySubscript { target, index } => {
27287            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27288        }
27289        Expr::ArraySlice { target, lo, hi } => {
27290            expr_has_toplevel_aggregate(target)
27291                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27292                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27293        }
27294        Expr::AnyAll { expr, array, .. } => {
27295            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27296        }
27297        Expr::Case {
27298            operand,
27299            branches,
27300            else_branch,
27301        } => {
27302            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27303                || branches
27304                    .iter()
27305                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27306                || else_branch
27307                    .as_deref()
27308                    .is_some_and(expr_has_toplevel_aggregate)
27309        }
27310        // The outer-level operands of a sublink can aggregate; the sublink's
27311        // own body cannot leak its aggregates up here.
27312        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27313        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27314            row.iter().any(expr_has_toplevel_aggregate)
27315        }
27316        _ => false,
27317    }
27318}
27319
27320/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27321/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27322/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27323/// sublink and is legal in a recursive term, so it is not walked here.
27324fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27325    let mut exprs: Vec<&Expr> = Vec::new();
27326    for it in &s.items {
27327        if let crate::ast::SelectItem::Expr { expr, .. } = it {
27328            exprs.push(expr);
27329        }
27330    }
27331    if let Some(w) = &s.where_ {
27332        exprs.push(w);
27333    }
27334    if let Some(h) = &s.having {
27335        exprs.push(h);
27336    }
27337    if let Some(g) = &s.group_by {
27338        exprs.extend(g.iter());
27339    }
27340    if let Some(from) = &s.from {
27341        for j in &from.joins {
27342            if let Some(on) = &j.on {
27343                exprs.push(on);
27344            }
27345        }
27346    }
27347    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27348}
27349
27350/// Does this expression contain a sublink whose subquery mentions `name`?
27351fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27352    match e {
27353        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27354        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27355        Expr::InSubquery { expr, subquery, .. } => {
27356            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27357        }
27358        Expr::RowInSubquery { row, subquery, .. } => {
27359            row.iter().any(|x| expr_sublink_mentions(x, name))
27360                || select_mentions_table(subquery, name)
27361        }
27362        Expr::RowCmpSubquery { row, subquery, .. } => {
27363            row.iter().any(|x| expr_sublink_mentions(x, name))
27364                || select_mentions_table(subquery, name)
27365        }
27366        Expr::NamedArg { expr, .. }
27367        | Expr::Variadic(expr)
27368        | Expr::Unary { expr, .. }
27369        | Expr::Cast { expr, .. }
27370        | Expr::IsNull { expr, .. }
27371        | Expr::FieldAccess { base: expr, .. }
27372        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27373        Expr::Binary { lhs, rhs, .. } => {
27374            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27375        }
27376        Expr::Like { expr, pattern, .. } => {
27377            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27378        }
27379        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27380            args.iter().any(|x| expr_sublink_mentions(x, name))
27381        }
27382        Expr::InList { expr, list, .. } => {
27383            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27384        }
27385        Expr::ArraySubscript { target, index } => {
27386            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27387        }
27388        Expr::ArraySlice { target, lo, hi } => {
27389            expr_sublink_mentions(target, name)
27390                || lo
27391                    .as_deref()
27392                    .is_some_and(|x| expr_sublink_mentions(x, name))
27393                || hi
27394                    .as_deref()
27395                    .is_some_and(|x| expr_sublink_mentions(x, name))
27396        }
27397        Expr::AnyAll { expr, array, .. } => {
27398            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27399        }
27400        Expr::Case {
27401            operand,
27402            branches,
27403            else_branch,
27404        } => {
27405            operand
27406                .as_deref()
27407                .is_some_and(|x| expr_sublink_mentions(x, name))
27408                || branches
27409                    .iter()
27410                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27411                || else_branch
27412                    .as_deref()
27413                    .is_some_and(|x| expr_sublink_mentions(x, name))
27414        }
27415        _ => false,
27416    }
27417}
27418
27419/// Does this SELECT (in full — FROM tables, derived tables, its own
27420/// sublinks, and union arms) mention the named table?
27421fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27422    if let Some(from) = &s.from {
27423        if from.primary.name.eq_ignore_ascii_case(name) {
27424            return true;
27425        }
27426        if let Some(sub) = &from.primary.lateral_subquery
27427            && select_mentions_table(sub, name)
27428        {
27429            return true;
27430        }
27431        for j in &from.joins {
27432            if j.table.name.eq_ignore_ascii_case(name) {
27433                return true;
27434            }
27435            if let Some(sub) = &j.table.lateral_subquery
27436                && select_mentions_table(sub, name)
27437            {
27438                return true;
27439            }
27440        }
27441    }
27442    if select_has_self_ref_in_sublink(s, name) {
27443        return true;
27444    }
27445    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27446}
27447
27448/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27449/// row count, the way PG evaluates one before applying it.
27450///
27451/// `None` = not a constant (a column, a subquery, a function call).
27452/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27453/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27454/// All wordings were read off live PG 18.4.
27455fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27456    use crate::ast::{BinOp, Expr, Literal, UnOp};
27457    match e {
27458        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27459        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27460            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27461        }
27462        // PG coerces a string by its CONTENT, and fails on the value.
27463        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27464            |_| {
27465                Err(alloc::format!(
27466                    "invalid input syntax for type bigint: \"{t}\""
27467                ))
27468            },
27469            |n| Ok(i128::from(n)),
27470        )),
27471        Expr::Literal(Literal::Bool(_)) => Some(Err(
27472            "argument of {L} must be type bigint, not type boolean".into(),
27473        )),
27474        Expr::Unary {
27475            op: UnOp::Neg,
27476            expr,
27477        } => match fold_limit_constant(expr)? {
27478            Ok(v) => Some(Ok(-v)),
27479            e @ Err(_) => Some(e),
27480        },
27481        Expr::Binary { lhs, op, rhs } => {
27482            let a = match fold_limit_constant(lhs)? {
27483                Ok(v) => v,
27484                e @ Err(_) => return Some(e),
27485            };
27486            let b = match fold_limit_constant(rhs)? {
27487                Ok(v) => v,
27488                e @ Err(_) => return Some(e),
27489            };
27490            let out = match op {
27491                BinOp::Add => a.checked_add(b),
27492                BinOp::Sub => a.checked_sub(b),
27493                BinOp::Mul => a.checked_mul(b),
27494                BinOp::Div if b != 0 => a.checked_div(b),
27495                BinOp::Div => return Some(Err("division by zero".into())),
27496                BinOp::Mod if b != 0 => a.checked_rem(b),
27497                BinOp::Mod => return Some(Err("division by zero".into())),
27498                _ => return None,
27499            };
27500            // PG evaluates the arithmetic in the operand's own type, so an
27501            // int-by-int product that leaves int range fails there — before
27502            // the row count is ever looked at.
27503            match out {
27504                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27505                    Some(Err("integer out of range".into()))
27506                }
27507                Some(v) => Some(Ok(v)),
27508                None => Some(Err("integer out of range".into())),
27509            }
27510        }
27511        _ => None,
27512    }
27513}
27514
27515/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27516/// cast, which is what makes `LIMIT 2.5` keep three rows.
27517fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27518    if scale == 0 {
27519        return unscaled;
27520    }
27521    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27522        return 0;
27523    };
27524    let neg = unscaled < 0;
27525    let mag = unscaled.unsigned_abs() as i128;
27526    let rounded = (mag + div / 2) / div;
27527    if neg { -rounded } else { rounded }
27528}
27529
27530#[cfg(test)]
27531mod tests {
27532    use super::*;
27533    use alloc::string::ToString;
27534
27535    fn parse(s: &str) -> Statement {
27536        parse_statement(s).expect("parse ok")
27537    }
27538
27539    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27540    // `tables`, `partition`, etc. are unreserved keywords per PG's
27541    // `pg_get_keywords()` and MUST be usable as column / table /
27542    // alias names. Pre-T4 every drop-in user whose schema had one
27543    // of these as a column name (sentori events.release, mailrs
27544    // messages.index in some forks) blew the parser up at CREATE
27545    // TABLE time with "expected identifier, got Release". The
27546    // generalisation lives in `unreserved_keyword_text` + the
27547    // `expect_ident_like` and `parse_atom` arms that consult it.
27548    #[test]
27549    fn release_usable_as_column_name_in_create_table() {
27550        let stmt =
27551            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27552        if let Statement::CreateTable(t) = stmt {
27553            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27554            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27555        } else {
27556            panic!("expected CreateTable");
27557        }
27558    }
27559
27560    #[test]
27561    fn release_usable_as_column_ref_in_select_projection() {
27562        // The sentori `0003_partition_events.sql` INSERT-SELECT
27563        // walk references `release` in both column lists; the
27564        // projection-side use exercises `parse_atom`'s relaxed
27565        // identifier set.
27566        parse("SELECT id, release, payload FROM events WHERE id = 1");
27567    }
27568
27569    #[test]
27570    fn release_usable_as_column_ref_in_insert_column_list() {
27571        // INSERT INTO t (id, release, payload) VALUES (…)
27572        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27573    }
27574
27575    #[test]
27576    fn alter_column_drop_not_null_uses_keyword_drop_token() {
27577        // Sentori `0013_audit_tombstone.sql` issues
27578        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27579        // emits Token::Drop (not Ident("drop")); the parser must
27580        // accept both in the ALTER COLUMN sub-dispatch.
27581        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27582    }
27583
27584    #[test]
27585    fn create_index_accepts_parenthesised_expression_key() {
27586        // sentori `0040_events_bundle_idx.sql` shape — JSONB
27587        // expression index. Pre-T4 the parser bailed at the
27588        // inner `(` with "expected column ident or expression,
27589        // got LParen". The Token::LParen arm in CREATE INDEX
27590        // routes through the expression parser instead.
27591        parse(
27592            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27593             ON events ((payload->'bundle'->>'id'))",
27594        );
27595    }
27596
27597    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27598    // surface as parse errors, never stack overflows (embed hosts
27599    // abort on overflow).
27600    /// The nesting budget is a COUNT; what it has to fit inside is a
27601    /// number of BYTES, and only one of those two is stable across
27602    /// compiler versions. Round 847 measured 30,336 bytes per level
27603    /// after a toolchain move, which puts 64 levels at 1.94 MB and
27604    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27605    /// aborted instead of erroring, which is precisely the outcome it
27606    /// exists to rule out.
27607    ///
27608    /// So the budget is metered rather than assumed. The ceiling leaves
27609    /// the depth SPG advertises fitting in a default 2 MiB thread with
27610    /// room to spare, in the debug build, where frames are widest.
27611    #[test]
27612    fn nesting_frame_cost_stays_under_ceiling() {
27613        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27614        // thread keeps a margin for whatever called the parser.
27615        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27616
27617        frame_meter::reset();
27618        let depth = frame_meter::SAMPLE_HI + 8;
27619        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27620        parse(&sql);
27621
27622        let per_level = frame_meter::bytes_per_level();
27623        {
27624            extern crate std;
27625            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27626        }
27627        assert!(
27628            per_level <= CEILING,
27629            "{per_level} bytes per nesting level exceeds {CEILING}; \
27630             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27631             in parse_expr_inner / parse_unary rather than lowering the \
27632             depth or widening the stack.",
27633            per_level * MAX_NEST_DEPTH
27634        );
27635    }
27636
27637    #[test]
27638    fn nesting_budget_errors_cleanly() {
27639        let depth = MAX_NEST_DEPTH + 50;
27640        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27641        let err = parse_statement(&sql).expect_err("must reject");
27642        assert!(err.message.contains("nests deeper"), "{err:?}");
27643        // Within budget still parses.
27644        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27645        parse(&sql);
27646    }
27647
27648    #[test]
27649    fn binary_chain_budget_errors_cleanly() {
27650        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27651        let err = parse_statement(&sql).expect_err("must reject");
27652        assert!(err.message.contains("chained binary"), "{err:?}");
27653        // Within budget still parses (chain depth ≤ budget is safe
27654        // for recursive eval/drop on 2 MiB stacks).
27655        let sql = format!("SELECT 1{}", " + 1".repeat(200));
27656        parse(&sql);
27657    }
27658
27659    #[test]
27660    fn in_list_unaffected_by_chain_budget() {
27661        // Flat InList: 20k elements parse fine and stay flat.
27662        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27663        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27664        let Statement::Select(s) = parse(&sql) else {
27665            panic!("expected select")
27666        };
27667        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27668            panic!("expected flat InList, got {:?}", s.where_)
27669        };
27670        assert_eq!(list.len(), 20_000);
27671        assert!(!negated);
27672    }
27673
27674    fn lit_int(n: i64) -> Expr {
27675        Expr::Literal(Literal::Integer(n))
27676    }
27677
27678    fn col(name: &str) -> Expr {
27679        Expr::Column(ColumnName {
27680            qualifier: None,
27681            name: name.into(),
27682        })
27683    }
27684
27685    #[test]
27686    fn select_single_integer() {
27687        let s = parse("SELECT 1");
27688        let Statement::Select(s) = s else {
27689            panic!("expected SELECT")
27690        };
27691        assert_eq!(s.items.len(), 1);
27692        assert!(s.from.is_none());
27693        assert!(s.where_.is_none());
27694    }
27695
27696    #[test]
27697    fn select_multiple_literal_kinds() {
27698        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27699        let Statement::Select(s) = s else {
27700            panic!("expected SELECT")
27701        };
27702        assert_eq!(s.items.len(), 5);
27703    }
27704
27705    #[test]
27706    fn select_wildcard_from_table() {
27707        let s = parse("SELECT * FROM users");
27708        let Statement::Select(s) = s else {
27709            panic!("expected SELECT")
27710        };
27711        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27712        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27713    }
27714
27715    #[test]
27716    fn select_with_table_alias() {
27717        let s = parse("SELECT * FROM users AS u");
27718        let Statement::Select(s) = s else {
27719            panic!("expected SELECT")
27720        };
27721        let t = &s.from.as_ref().unwrap().primary;
27722        assert_eq!(t.name, "users");
27723        assert_eq!(t.alias.as_deref(), Some("u"));
27724    }
27725
27726    #[test]
27727    fn select_with_where_eq() {
27728        let s = parse("SELECT a FROM t WHERE a = 1");
27729        let Statement::Select(s) = s else {
27730            panic!("expected SELECT")
27731        };
27732        let w = s.where_.unwrap();
27733        assert_eq!(
27734            w,
27735            Expr::Binary {
27736                lhs: Box::new(col("a")),
27737                op: BinOp::Eq,
27738                rhs: Box::new(lit_int(1)),
27739            }
27740        );
27741    }
27742
27743    #[test]
27744    fn arithmetic_precedence() {
27745        let s = parse("SELECT 1 + 2 * 3");
27746        let Statement::Select(s) = s else {
27747            panic!("expected SELECT")
27748        };
27749        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27750            panic!("wildcard?")
27751        };
27752        assert_eq!(
27753            expr,
27754            &Expr::Binary {
27755                lhs: Box::new(lit_int(1)),
27756                op: BinOp::Add,
27757                rhs: Box::new(Expr::Binary {
27758                    lhs: Box::new(lit_int(2)),
27759                    op: BinOp::Mul,
27760                    rhs: Box::new(lit_int(3)),
27761                }),
27762            }
27763        );
27764    }
27765
27766    #[test]
27767    fn parentheses_override_precedence() {
27768        let s = parse("SELECT (1 + 2) * 3");
27769        let Statement::Select(s) = s else {
27770            panic!("expected SELECT")
27771        };
27772        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27773            panic!()
27774        };
27775        assert_eq!(
27776            expr,
27777            &Expr::Binary {
27778                lhs: Box::new(Expr::Binary {
27779                    lhs: Box::new(lit_int(1)),
27780                    op: BinOp::Add,
27781                    rhs: Box::new(lit_int(2)),
27782                }),
27783                op: BinOp::Mul,
27784                rhs: Box::new(lit_int(3)),
27785            }
27786        );
27787    }
27788
27789    #[test]
27790    fn not_binds_below_comparison() {
27791        // `NOT a = 1` should parse as `NOT (a = 1)`.
27792        let s = parse("SELECT NOT a = 1 FROM t");
27793        let Statement::Select(s) = s else {
27794            panic!("expected SELECT")
27795        };
27796        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27797            panic!()
27798        };
27799        assert_eq!(
27800            expr,
27801            &Expr::Unary {
27802                op: UnOp::Not,
27803                expr: Box::new(Expr::Binary {
27804                    lhs: Box::new(col("a")),
27805                    op: BinOp::Eq,
27806                    rhs: Box::new(lit_int(1)),
27807                }),
27808            }
27809        );
27810    }
27811
27812    #[test]
27813    fn unary_minus_binds_above_multiplication() {
27814        // `-a * 2` should be `(-a) * 2`.
27815        let s = parse("SELECT -a * 2 FROM t");
27816        let Statement::Select(s) = s else {
27817            panic!("expected SELECT")
27818        };
27819        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27820            panic!()
27821        };
27822        assert_eq!(
27823            expr,
27824            &Expr::Binary {
27825                lhs: Box::new(Expr::Unary {
27826                    op: UnOp::Neg,
27827                    expr: Box::new(col("a")),
27828                }),
27829                op: BinOp::Mul,
27830                rhs: Box::new(lit_int(2)),
27831            }
27832        );
27833    }
27834
27835    #[test]
27836    fn qualified_column() {
27837        let s = parse("SELECT t.col FROM t");
27838        let Statement::Select(s) = s else {
27839            panic!("expected SELECT")
27840        };
27841        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27842            panic!()
27843        };
27844        assert_eq!(
27845            expr,
27846            &Expr::Column(ColumnName {
27847                qualifier: Some("t".into()),
27848                name: "col".into()
27849            })
27850        );
27851    }
27852
27853    #[test]
27854    fn select_item_alias_with_as() {
27855        let s = parse("SELECT a AS y FROM t");
27856        let Statement::Select(s) = s else {
27857            panic!("expected SELECT")
27858        };
27859        let SelectItem::Expr { alias, .. } = &s.items[0] else {
27860            panic!()
27861        };
27862        assert_eq!(alias.as_deref(), Some("y"));
27863    }
27864
27865    #[test]
27866    fn trailing_semicolon_accepted() {
27867        let s = parse("SELECT 1;");
27868        let Statement::Select(s) = s else {
27869            panic!("expected SELECT")
27870        };
27871        assert_eq!(s.items.len(), 1);
27872    }
27873
27874    #[test]
27875    fn boolean_chain_with_and_or_not() {
27876        // (NOT a) OR (b AND (NOT c))
27877        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27878        let Statement::Select(s) = s else {
27879            panic!("expected SELECT")
27880        };
27881        let SelectItem::Expr { expr, .. } = &s.items[0] else {
27882            panic!()
27883        };
27884        let expected = Expr::Binary {
27885            lhs: Box::new(Expr::Unary {
27886                op: UnOp::Not,
27887                expr: Box::new(col("a")),
27888            }),
27889            op: BinOp::Or,
27890            rhs: Box::new(Expr::Binary {
27891                lhs: Box::new(col("b")),
27892                op: BinOp::And,
27893                rhs: Box::new(Expr::Unary {
27894                    op: UnOp::Not,
27895                    expr: Box::new(col("c")),
27896                }),
27897            }),
27898        };
27899        assert_eq!(expr, &expected);
27900    }
27901
27902    #[test]
27903    fn empty_input_errors() {
27904        // v7.14.0 — pg_dump preambles emit several comment-only
27905        // / blank-line statements that collapse to Statement::
27906        // Empty rather than a parse error. The old "SELECT in
27907        // message" assertion is stale; verify the new contract:
27908        // empty / whitespace / comment-only input parses to
27909        // Statement::Empty.
27910        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27911        assert!(matches!(
27912            parse_statement("  \n\t ").unwrap(),
27913            Statement::Empty
27914        ));
27915        // Sanity: malformed-but-non-empty still errors.
27916        assert!(parse_statement("SELECT FROM WHERE").is_err());
27917    }
27918
27919    #[test]
27920    fn unmatched_paren_errors() {
27921        assert!(parse_statement("SELECT (1 + 2").is_err());
27922    }
27923
27924    #[test]
27925    fn display_round_trip_simple_select() {
27926        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27927        let text = original.to_string();
27928        let again = parse_statement(&text).expect("re-parse");
27929        assert_eq!(original, again);
27930    }
27931
27932    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27933
27934    #[test]
27935    fn create_table_single_column() {
27936        let s = parse("CREATE TABLE foo (a INT)");
27937        let Statement::CreateTable(c) = s else {
27938            panic!("expected CreateTable")
27939        };
27940        assert_eq!(c.name, "foo");
27941        assert_eq!(c.columns.len(), 1);
27942        assert_eq!(c.columns[0].name, "a");
27943        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27944        assert!(c.columns[0].nullable);
27945    }
27946
27947    #[test]
27948    fn create_table_multi_column_with_not_null_mix() {
27949        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27950        let Statement::CreateTable(c) = s else {
27951            panic!()
27952        };
27953        assert_eq!(c.columns.len(), 4);
27954        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27955        assert!(!c.columns[0].nullable);
27956        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27957        assert!(c.columns[1].nullable);
27958        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27959        assert!(!c.columns[2].nullable);
27960        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27961    }
27962
27963    #[test]
27964    fn create_table_bigint_supported() {
27965        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27966        let Statement::CreateTable(c) = s else {
27967            panic!()
27968        };
27969        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27970    }
27971
27972    #[test]
27973    fn create_table_vector_default_is_f32() {
27974        let s = parse("CREATE TABLE t (v VECTOR(128))");
27975        let Statement::CreateTable(c) = s else {
27976            panic!()
27977        };
27978        assert_eq!(
27979            c.columns[0].ty,
27980            ColumnTypeName::Vector {
27981                dim: 128,
27982                encoding: VecEncoding::F32,
27983            },
27984        );
27985    }
27986
27987    #[test]
27988    fn create_table_vector_using_sq8() {
27989        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27990        // Case-insensitive on both `USING` and the encoding name.
27991        for sql in [
27992            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27993            "CREATE TABLE t (v VECTOR(128) using sq8)",
27994        ] {
27995            let s = parse(sql);
27996            let Statement::CreateTable(c) = s else {
27997                panic!()
27998            };
27999            assert_eq!(
28000                c.columns[0].ty,
28001                ColumnTypeName::Vector {
28002                    dim: 128,
28003                    encoding: VecEncoding::Sq8,
28004                },
28005                "{sql}",
28006            );
28007        }
28008    }
28009
28010    #[test]
28011    fn create_table_vector_using_unknown_errors() {
28012        // v7.16.1 — the inline `USING <encoding>` shape on
28013        // CREATE TABLE column defs was withdrawn before
28014        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28015        // (col vector_<metric>_ops)`; the parser now rejects
28016        // USING at column-list position with a clearer
28017        // "expected ',' or ')'" message. Test asserts the
28018        // current rejection, not the old "unknown vector
28019        // encoding" string.
28020        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28021        assert!(
28022            err.message.contains("USING")
28023                || err.message.contains("using")
28024                || err.message.contains("')'")
28025                || err.message.contains("','"),
28026            "expected USING/column-list rejection, got: {}",
28027            err.message
28028        );
28029    }
28030
28031    #[test]
28032    fn vector_using_sq8_display_roundtrips() {
28033        // The Display impl must produce text that re-parses to the
28034        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28035        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28036        let Statement::CreateTable(c) = s else {
28037            panic!()
28038        };
28039        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28040    }
28041
28042    #[test]
28043    fn parser_recognises_placeholders() {
28044        use crate::ast::{Expr, SelectItem, Statement};
28045        // $N in expression position parses as Expr::Placeholder(N).
28046        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28047        let Statement::Select(sel) = s else { panic!() };
28048        assert!(matches!(
28049            sel.items[0],
28050            SelectItem::Expr {
28051                expr: Expr::Placeholder(1),
28052                alias: None
28053            }
28054        ));
28055        // $2 + 1
28056        let SelectItem::Expr {
28057            expr: Expr::Binary { lhs, rhs, .. },
28058            ..
28059        } = &sel.items[1]
28060        else {
28061            panic!()
28062        };
28063        assert!(matches!(**lhs, Expr::Placeholder(2)));
28064        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28065        // WHERE x = $3
28066        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28067            panic!()
28068        };
28069        assert!(matches!(**rhs, Expr::Placeholder(3)));
28070    }
28071
28072    #[test]
28073    fn parser_rejects_dollar_zero() {
28074        // $0 is not valid in PG; the lexer rejects it.
28075        assert!(parse_statement("SELECT $0").is_err());
28076    }
28077
28078    #[test]
28079    fn placeholder_display_roundtrips() {
28080        // The Display impl must produce text that re-lexes to the
28081        // same Placeholder token.
28082        let s = parse("SELECT $42 FROM t");
28083        let printed = s.to_string();
28084        assert!(printed.contains("$42"));
28085        let again = parse(&printed);
28086        assert_eq!(s, again);
28087    }
28088
28089    #[test]
28090    fn alter_index_rebuild_bare() {
28091        use crate::ast::{AlterIndexTarget, Statement};
28092        let s = parse("ALTER INDEX my_idx REBUILD");
28093        let Statement::AlterIndex(a) = s else {
28094            panic!("expected AlterIndex, got {s:?}")
28095        };
28096        assert_eq!(a.name, "my_idx");
28097        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28098    }
28099
28100    #[test]
28101    fn alter_index_rebuild_with_encoding() {
28102        use crate::ast::{AlterIndexTarget, Statement};
28103        for (sql, want) in [
28104            (
28105                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28106                VecEncoding::F32,
28107            ),
28108            (
28109                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28110                VecEncoding::Sq8,
28111            ),
28112            (
28113                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28114                VecEncoding::F16,
28115            ),
28116        ] {
28117            let s = parse(sql);
28118            let Statement::AlterIndex(a) = s else {
28119                panic!("{sql}: expected AlterIndex")
28120            };
28121            assert_eq!(a.name, "my_idx");
28122            assert_eq!(
28123                a.target,
28124                AlterIndexTarget::Rebuild {
28125                    encoding: Some(want)
28126                },
28127                "{sql}"
28128            );
28129        }
28130    }
28131
28132    #[test]
28133    fn alter_index_rebuild_unknown_encoding_errors() {
28134        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28135        assert!(
28136            err.message.contains("unknown vector encoding"),
28137            "got: {}",
28138            err.message
28139        );
28140    }
28141
28142    #[test]
28143    fn alter_index_rebuild_display_roundtrips() {
28144        for (input, want) in [
28145            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28146            (
28147                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28148                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28149            ),
28150            (
28151                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28152                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28153            ),
28154        ] {
28155            let s = parse(input);
28156            assert_eq!(s.to_string(), want);
28157        }
28158    }
28159
28160    #[test]
28161    fn create_table_unknown_type_defers_to_engine() {
28162        // v4.9 picked XML as a parse-time "unsupported column
28163        // type" probe. v7.17.0 Phase 1.4 changed the contract:
28164        // an unknown type ident parses as Text + `user_type_ref`
28165        // so CREATE TABLE can resolve user-defined enum / domain
28166        // types — rejection of truly-unknown types moved to the
28167        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28168        // to a first-class built-in, so this probe switched to a
28169        // synthetic name nothing in the lexer will ever recognise.
28170        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28171        let Statement::CreateTable(t) = stmt else {
28172            panic!("expected CreateTable");
28173        };
28174        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28175    }
28176
28177    #[test]
28178    fn create_table_missing_table_keyword_errors() {
28179        assert!(parse_statement("CREATE x (a INT)").is_err());
28180    }
28181
28182    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28183    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28184
28185    #[test]
28186    fn parse_create_table_partition_by_range() {
28187        use crate::ast::{PartitionBySpec, PartitionKindAst};
28188        let stmt = parse_statement(
28189            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28190             payload JSONB) PARTITION BY RANGE (ts)",
28191        )
28192        .unwrap();
28193        let Statement::CreateTable(t) = stmt else {
28194            panic!("expected CreateTable");
28195        };
28196        assert!(t.partition_of.is_none(), "parent has no partition_of");
28197        assert_eq!(t.columns.len(), 3);
28198        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28199        assert_eq!(
28200            by,
28201            &PartitionBySpec {
28202                kind: PartitionKindAst::Range,
28203                key_columns: alloc::vec!["ts".to_string()],
28204            }
28205        );
28206        // Display round-trip preserves the suffix. `quote_ident`
28207        // only adds double quotes when the ident needs escaping, so
28208        // a plain `ts` survives bare here.
28209        assert!(
28210            t.to_string().contains("PARTITION BY RANGE (ts)"),
28211            "Display lost PARTITION BY suffix: {t}"
28212        );
28213    }
28214
28215    #[test]
28216    fn parse_create_table_partition_of_range() {
28217        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28218        let stmt = parse_statement(
28219            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28220             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28221        )
28222        .unwrap();
28223        let Statement::CreateTable(t) = stmt else {
28224            panic!("expected CreateTable");
28225        };
28226        assert!(t.columns.is_empty(), "child inherits columns from parent");
28227        assert!(t.partition_by.is_none());
28228        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28229        assert_eq!(of.parent_name, "events_partitioned");
28230        let PartitionOfSpec { bounds, .. } = of.clone();
28231        match bounds {
28232            PartitionOfBoundsAst::Range { lower, upper } => {
28233                assert!(lower.to_string().contains("2026-06-01"));
28234                assert!(upper.to_string().contains("2026-07-01"));
28235            }
28236            other => panic!("expected Range, got {other:?}"),
28237        }
28238        // Display round-trip emits the FOR VALUES tail. `quote_ident`
28239        // skips quotes when not required, so the parent name appears
28240        // bare here.
28241        let s = t.to_string();
28242        assert!(
28243            s.contains("PARTITION OF events_partitioned"),
28244            "Display lost PARTITION OF: {s}"
28245        );
28246        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28247        assert!(s.contains(") TO ("), "Display lost TO: {s}");
28248    }
28249
28250    #[test]
28251    fn parse_create_table_partition_of_default() {
28252        use crate::ast::PartitionOfBoundsAst;
28253        let stmt =
28254            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28255                .unwrap();
28256        let Statement::CreateTable(t) = stmt else {
28257            panic!("expected CreateTable");
28258        };
28259        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28260        assert_eq!(of.parent_name, "events_partitioned");
28261        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28262        assert!(
28263            t.to_string()
28264                .contains("PARTITION OF events_partitioned DEFAULT"),
28265            "Display lost DEFAULT: {t}"
28266        );
28267    }
28268
28269    #[test]
28270    fn parse_create_table_partition_by_list() {
28271        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28272        // child with `FOR VALUES IN (lit, lit, …)`.
28273        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28274        let parent =
28275            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28276                .unwrap();
28277        let Statement::CreateTable(t) = parent else {
28278            panic!("expected CreateTable");
28279        };
28280        let Some(PartitionBySpec {
28281            kind,
28282            ref key_columns,
28283        }) = t.partition_by
28284        else {
28285            panic!("expected PARTITION BY");
28286        };
28287        assert_eq!(kind, PartitionKindAst::List);
28288        assert_eq!(*key_columns, vec!["region".to_string()]);
28289        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28290
28291        let child = parse_statement(
28292            "CREATE TABLE events_apac PARTITION OF events_listed \
28293             FOR VALUES IN ('jp', 'kr', 'tw')",
28294        )
28295        .unwrap();
28296        let Statement::CreateTable(c) = child else {
28297            panic!("expected CreateTable");
28298        };
28299        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28300        let PartitionOfBoundsAst::List { values } = &of.bounds else {
28301            panic!("expected List bounds, got {:?}", of.bounds);
28302        };
28303        assert_eq!(values.len(), 3);
28304        let disp = c.to_string();
28305        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28306    }
28307
28308    #[test]
28309    fn parse_create_table_partition_by_hash() {
28310        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28311        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28312        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28313        let parent =
28314            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28315        let Statement::CreateTable(t) = parent else {
28316            panic!("expected CreateTable");
28317        };
28318        let Some(PartitionBySpec {
28319            kind,
28320            ref key_columns,
28321        }) = t.partition_by
28322        else {
28323            panic!("expected PARTITION BY");
28324        };
28325        assert_eq!(kind, PartitionKindAst::Hash);
28326        assert_eq!(*key_columns, vec!["id".to_string()]);
28327        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28328
28329        let child = parse_statement(
28330            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28331             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28332        )
28333        .unwrap();
28334        let Statement::CreateTable(c) = child else {
28335            panic!("expected CreateTable");
28336        };
28337        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28338        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28339            panic!("expected Hash bounds");
28340        };
28341        assert_eq!(modulus, 4);
28342        assert_eq!(remainder, 0);
28343        let disp = c.to_string();
28344        assert!(
28345            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28346            "Display lost HASH bounds: {disp}"
28347        );
28348
28349        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28350        let bad = parse_statement(
28351            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28352             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28353        );
28354        let msg = format!("{}", bad.unwrap_err());
28355        assert!(
28356            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28357            "expected REMAINDER/MODULUS validation error: {msg}"
28358        );
28359    }
28360
28361    #[test]
28362    fn parse_create_table_partition_of_rejects_columns() {
28363        // v7.37.6-B contract: PARTITION OF children inherit columns
28364        // from the parent; an explicit list MUST surface as a parse
28365        // error rather than getting silently ignored.
28366        let err = parse_statement(
28367            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28368             FOR VALUES FROM ('a') TO ('b')",
28369        );
28370        assert!(err.is_err(), "expected parse error for explicit columns");
28371        let msg = format!("{}", err.unwrap_err());
28372        assert!(
28373            msg.contains("PARTITION OF") && msg.contains("column"),
28374            "error should mention PARTITION OF + columns: {msg}"
28375        );
28376    }
28377
28378    #[test]
28379    fn insert_single_value() {
28380        let s = parse("INSERT INTO foo VALUES (42)");
28381        let Statement::Insert(i) = s else {
28382            panic!("expected Insert")
28383        };
28384        assert_eq!(i.table, "foo");
28385        assert_eq!(i.rows.len(), 1);
28386        assert_eq!(i.rows[0].len(), 1);
28387        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28388    }
28389
28390    #[test]
28391    fn insert_multi_value_with_mixed_literals() {
28392        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28393        let Statement::Insert(i) = s else { panic!() };
28394        assert_eq!(i.rows.len(), 1);
28395        assert_eq!(i.rows[0].len(), 5);
28396    }
28397
28398    #[test]
28399    fn insert_missing_into_errors() {
28400        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28401    }
28402
28403    #[test]
28404    fn create_table_round_trip() {
28405        let original =
28406            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28407        let text = original.to_string();
28408        let again = parse_statement(&text).expect("re-parse");
28409        assert_eq!(original, again);
28410    }
28411
28412    #[test]
28413    fn insert_round_trip_with_negation_and_string() {
28414        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28415        let text = original.to_string();
28416        let again = parse_statement(&text).expect("re-parse");
28417        assert_eq!(original, again);
28418    }
28419
28420    #[test]
28421    fn unknown_keyword_at_statement_start_errors() {
28422        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28423        // the top-level dispatch still has no branch to take.
28424        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28425        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28426    }
28427
28428    // --- v0.8 CREATE INDEX --------------------------------------------------
28429
28430    #[test]
28431    fn create_index_basic() {
28432        let s = parse("CREATE INDEX idx_id ON users (id)");
28433        let Statement::CreateIndex(c) = s else {
28434            panic!("expected CreateIndex")
28435        };
28436        assert_eq!(c.name, "idx_id");
28437        assert_eq!(c.table, "users");
28438        assert_eq!(c.column, "id");
28439    }
28440
28441    #[test]
28442    fn create_index_missing_on_errors() {
28443        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28444    }
28445
28446    #[test]
28447    fn create_index_missing_paren_errors() {
28448        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28449    }
28450
28451    #[test]
28452    fn create_index_round_trip() {
28453        let original = parse("CREATE INDEX by_name ON users (name)");
28454        let again = parse_statement(&original.to_string()).unwrap();
28455        assert_eq!(original, again);
28456    }
28457
28458    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28459
28460    #[test]
28461    fn create_unique_index_basic() {
28462        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28463        let Statement::CreateIndex(c) = s else {
28464            panic!("expected CreateIndex");
28465        };
28466        assert!(c.is_unique);
28467        assert_eq!(c.column, "a");
28468        assert!(c.partial_predicate.is_none());
28469    }
28470
28471    #[test]
28472    fn create_unique_index_partial() {
28473        // mailrs's email_templates "one default per user" shape.
28474        let s = parse(
28475            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28476             ON email_templates (user_address) WHERE is_default = true",
28477        );
28478        let Statement::CreateIndex(c) = s else {
28479            panic!("expected CreateIndex");
28480        };
28481        assert!(c.is_unique);
28482        assert_eq!(c.table, "email_templates");
28483        assert_eq!(c.column, "user_address");
28484        assert!(c.partial_predicate.is_some());
28485    }
28486
28487    #[test]
28488    fn create_unique_index_composite_with_predicate() {
28489        // mailrs's calendar_events instance: composite columns.
28490        let s = parse(
28491            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28492             ON calendar_events (calendar_id, uid, recurrence_id) \
28493             WHERE recurrence_id IS NOT NULL",
28494        );
28495        let Statement::CreateIndex(c) = s else {
28496            panic!("expected CreateIndex");
28497        };
28498        assert!(c.is_unique);
28499        assert_eq!(c.column, "calendar_id");
28500        assert_eq!(
28501            c.extra_columns,
28502            vec!["uid".to_string(), "recurrence_id".to_string()]
28503        );
28504        assert!(c.partial_predicate.is_some());
28505    }
28506
28507    #[test]
28508    fn create_unique_index_using_btree_ok() {
28509        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28510        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28511    }
28512
28513    #[test]
28514    fn create_unique_index_using_hnsw_rejected() {
28515        let err =
28516            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28517        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28518    }
28519
28520    #[test]
28521    fn create_unique_index_round_trip() {
28522        let original = parse(
28523            "CREATE UNIQUE INDEX uq_calendar_events_master \
28524             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28525        );
28526        let again = parse_statement(&original.to_string()).unwrap();
28527        assert_eq!(original, again);
28528    }
28529
28530    #[test]
28531    fn create_unique_without_index_errors() {
28532        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28533        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28534        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28535    }
28536
28537    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28538
28539    #[test]
28540    fn create_table_bytea_column() {
28541        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28542        let Statement::CreateTable(c) = s else {
28543            panic!("expected CreateTable");
28544        };
28545        assert_eq!(c.columns.len(), 2);
28546        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28547        assert!(!c.columns[1].nullable);
28548    }
28549
28550    #[test]
28551    fn create_table_bytes_alias_column() {
28552        let s = parse("CREATE TABLE t (blob BYTES)");
28553        let Statement::CreateTable(c) = s else {
28554            panic!("expected CreateTable");
28555        };
28556        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28557    }
28558
28559    #[test]
28560    fn bytea_round_trip_display() {
28561        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28562        let again = parse_statement(&original.to_string()).unwrap();
28563        assert_eq!(original, again);
28564    }
28565
28566    // --- v0.9 transactions -------------------------------------------------
28567
28568    #[test]
28569    fn begin_commit_rollback_parse_as_unit_variants() {
28570        let plain = crate::ast::TransactionModes::default();
28571        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
28572        assert_eq!(parse("COMMIT"), Statement::Commit);
28573        // r1066 — PG synonyms pgbench's tpcb script relies on.
28574        assert_eq!(parse("END"), Statement::Commit);
28575        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
28576        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
28577        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28578        // Trailing semicolons accepted too.
28579        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
28580        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28581        // statement (with or without the WORK/TRANSACTION noise word).
28582        assert_eq!(
28583            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28584            Statement::Begin(crate::ast::TransactionModes {
28585                isolation: Some(IsolationLevel::RepeatableRead),
28586                read_only: None,
28587            })
28588        );
28589        assert_eq!(
28590            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28591            Statement::Begin(crate::ast::TransactionModes {
28592                isolation: Some(IsolationLevel::Serializable),
28593                read_only: None,
28594            })
28595        );
28596        // v7.39 — this line used to read
28597        //
28598        //     // A non-isolation mode keeps the session default (None).
28599        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28600        //
28601        // which pinned the defect rather than catching it: the READ ONLY
28602        // was thrown away, so the statement opened an ordinary read-write
28603        // transaction and every write inside it was accepted. The
28604        // isolation level is still absent here, because this statement
28605        // does not name one — that part was right.
28606        assert_eq!(
28607            parse("BEGIN READ ONLY"),
28608            Statement::Begin(crate::ast::TransactionModes {
28609                isolation: None,
28610                read_only: Some(true),
28611            })
28612        );
28613        assert_eq!(
28614            parse("START TRANSACTION READ WRITE"),
28615            Statement::Begin(crate::ast::TransactionModes {
28616                isolation: None,
28617                read_only: Some(false),
28618            })
28619        );
28620        assert_eq!(
28621            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
28622            Statement::Begin(crate::ast::TransactionModes {
28623                isolation: Some(IsolationLevel::Serializable),
28624                read_only: Some(true),
28625            })
28626        );
28627    }
28628
28629    // --- v1.2: pgvector distance ops + ::vector cast --------------------
28630
28631    #[test]
28632    fn inner_product_binop_parses() {
28633        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28634        let Statement::Select(s) = s else { panic!() };
28635        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28636            panic!()
28637        };
28638        assert!(matches!(
28639            expr,
28640            Expr::Binary {
28641                op: BinOp::InnerProduct,
28642                ..
28643            }
28644        ));
28645    }
28646
28647    #[test]
28648    fn cosine_distance_binop_parses() {
28649        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28650        let Statement::Select(s) = s else { panic!() };
28651        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28652            panic!()
28653        };
28654        assert!(matches!(
28655            expr,
28656            Expr::Binary {
28657                op: BinOp::CosineDistance,
28658                ..
28659            }
28660        ));
28661    }
28662
28663    #[test]
28664    fn vector_cast_postfix_wraps_string_literal() {
28665        let s = parse("SELECT '[1,2,3]'::vector FROM t");
28666        let Statement::Select(s) = s else { panic!() };
28667        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28668            panic!()
28669        };
28670        assert!(matches!(
28671            expr,
28672            Expr::Cast {
28673                target: CastTarget::Vector,
28674                ..
28675            }
28676        ));
28677    }
28678
28679    #[test]
28680    fn unsupported_cast_target_errors() {
28681        // v7.37.5 ship triage promoted the parser to accept every
28682        // ident as a `CastTarget::Named(canonical)`; the engine
28683        // surfaces the "unsupported cast target" error at eval
28684        // time when `type_name_to_data_type` can't resolve it.
28685        // Parser-side error now requires a NON-ident after `::`
28686        // (e.g. a punctuation token).
28687        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28688        assert_eq!(err.message, "syntax error at or near \",\"");
28689    }
28690
28691    #[test]
28692    fn tx_statements_round_trip() {
28693        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28694            let original = parse(q);
28695            let again = parse_statement(&original.to_string()).unwrap();
28696            assert_eq!(original, again);
28697        }
28698    }
28699
28700    #[test]
28701    fn interval_text_parsing_units() {
28702        // v7.37.5 β — three-field shape `(months, days, micros)` so
28703        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28704        // Single unit.
28705        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28706        assert_eq!(
28707            parse_interval_text("24 hours"),
28708            Some((0, 0, 86_400_000_000))
28709        );
28710        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28711        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28712        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28713        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28714        // Compound spans accumulate per-dimension.
28715        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28716        assert_eq!(
28717            parse_interval_text("1 day 2 hours"),
28718            Some((0, 1, 7_200_000_000))
28719        );
28720        // Negative numbers carry through per-dimension.
28721        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28722        // Bad shapes return None.
28723        assert_eq!(parse_interval_text(""), None);
28724        assert_eq!(parse_interval_text("garbage"), None);
28725        assert_eq!(parse_interval_text("1 fortnight"), None);
28726        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28727        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28728        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28729        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28730        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28731    }
28732
28733    #[test]
28734    fn interval_literal_roundtrips_via_display() {
28735        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28736        let s = parsed.to_string();
28737        // Display preserves the original text verbatim.
28738        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28739        // And re-parsing yields a structurally equal statement.
28740        let again = parse_statement(&s).unwrap();
28741        assert_eq!(parsed, again);
28742    }
28743
28744    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28745
28746    #[test]
28747    fn parser_recognises_create_publication_bare() {
28748        let s = parse("CREATE PUBLICATION pub_a");
28749        let Statement::CreatePublication(p) = s else {
28750            panic!("expected CreatePublication, got {s:?}")
28751        };
28752        assert_eq!(p.name, "pub_a");
28753        assert_eq!(p.scope, PublicationScope::AllTables);
28754    }
28755
28756    #[test]
28757    fn parser_recognises_create_publication_for_all_tables() {
28758        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28759        let Statement::CreatePublication(p) = s else {
28760            panic!("expected CreatePublication, got {s:?}")
28761        };
28762        assert_eq!(p.name, "pub_a");
28763        assert_eq!(p.scope, PublicationScope::AllTables);
28764    }
28765
28766    #[test]
28767    fn parser_recognises_drop_publication() {
28768        let s = parse("DROP PUBLICATION pub_a");
28769        let Statement::DropPublication { name, .. } = s else {
28770            panic!("expected DropPublication, got {s:?}")
28771        };
28772        assert_eq!(name, "pub_a");
28773    }
28774
28775    #[test]
28776    fn parser_recognises_for_table_list() {
28777        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28778        let Statement::CreatePublication(p) = s else {
28779            panic!("expected CreatePublication, got {s:?}")
28780        };
28781        assert_eq!(p.name, "pub_a");
28782        let PublicationScope::ForTables(ts) = p.scope else {
28783            panic!("expected ForTables scope")
28784        };
28785        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28786    }
28787
28788    #[test]
28789    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28790        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28791        // is rejected (`invalid publication object list`; the old
28792        // test pinned an unverifiable "PG 19 accepts both" claim);
28793        // TABLES pairs with IN SCHEMA.
28794        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28795            .expect_err("bare FOR TABLES must reject");
28796        assert!(
28797            alloc::format!("{err}").contains("invalid publication object list"),
28798            "got: {err}"
28799        );
28800        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28801        let Statement::CreatePublication(p) = s else {
28802            panic!("expected CreatePublication, got {s:?}")
28803        };
28804        let PublicationScope::TablesInSchema(schema) = p.scope else {
28805            panic!("expected TablesInSchema")
28806        };
28807        assert_eq!(schema, "public");
28808    }
28809
28810    #[test]
28811    fn parser_recognises_for_all_tables_except_list() {
28812        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28813        let Statement::CreatePublication(p) = s else {
28814            panic!()
28815        };
28816        let PublicationScope::AllTablesExcept(ts) = p.scope else {
28817            panic!("expected AllTablesExcept")
28818        };
28819        assert_eq!(ts, alloc::vec!["t1", "t2"]);
28820    }
28821
28822    #[test]
28823    fn parser_rejects_for_table_with_empty_list() {
28824        // `FOR TABLE` with nothing after is a parse error.
28825        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28826            .expect_err("must error on empty list");
28827        // No specific message asserted — the call falls through to
28828        // expect_ident_like which yields "expected identifier, got …".
28829        assert!(!err.message.is_empty());
28830    }
28831
28832    #[test]
28833    fn parser_recognises_show_publications() {
28834        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28835        // bare ident in this position, NOT a reserved keyword.
28836        let s = parse("SHOW PUBLICATIONS");
28837        assert!(matches!(s, Statement::ShowPublications));
28838    }
28839
28840    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28841
28842    #[test]
28843    fn parser_recognises_create_subscription_single_publication() {
28844        let s = parse(
28845            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28846        );
28847        let Statement::CreateSubscription(c) = s else {
28848            panic!("expected CreateSubscription, got {s:?}")
28849        };
28850        assert_eq!(c.name, "sub_a");
28851        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28852        assert_eq!(c.publications, alloc::vec!["pub_a"]);
28853    }
28854
28855    #[test]
28856    fn parser_recognises_create_subscription_multi_publication() {
28857        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28858        let Statement::CreateSubscription(c) = s else {
28859            panic!()
28860        };
28861        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28862    }
28863
28864    #[test]
28865    fn parser_rejects_create_subscription_missing_connection() {
28866        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28867            .expect_err("must error on missing CONNECTION");
28868        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28869    }
28870
28871    #[test]
28872    fn parser_rejects_create_subscription_missing_publication() {
28873        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28874            .expect_err("must error on missing PUBLICATION");
28875        assert_eq!(err.message, "syntax error at end of input");
28876    }
28877
28878    #[test]
28879    fn parser_recognises_drop_subscription() {
28880        let s = parse("DROP SUBSCRIPTION sub_a");
28881        let Statement::DropSubscription { name, .. } = s else {
28882            panic!("expected DropSubscription, got {s:?}")
28883        };
28884        assert_eq!(name, "sub_a");
28885    }
28886
28887    #[test]
28888    fn parser_recognises_show_subscriptions() {
28889        let s = parse("SHOW SUBSCRIPTIONS");
28890        assert!(matches!(s, Statement::ShowSubscriptions));
28891    }
28892
28893    #[test]
28894    fn parser_recognises_wait_for_wal_position_no_timeout() {
28895        let s = parse("WAIT FOR WAL POSITION 12345");
28896        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28897            panic!("expected WaitForWalPosition, got {s:?}")
28898        };
28899        assert_eq!(pos, 12345);
28900        assert!(timeout_ms.is_none());
28901    }
28902
28903    #[test]
28904    fn parser_recognises_wait_for_wal_position_with_timeout() {
28905        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28906        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28907            panic!()
28908        };
28909        assert_eq!(pos, 67890);
28910        assert_eq!(timeout_ms, Some(5000));
28911    }
28912
28913    #[test]
28914    fn parser_rejects_wait_with_negative_position() {
28915        // The lexer treats `-` as a token; `expect_u64_literal`
28916        // only sees the Integer that follows, so the negative
28917        // arrives as a unary-minus expression at higher levels.
28918        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28919        // parse error one way or another.
28920        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28921        assert!(!err.message.is_empty());
28922    }
28923
28924    #[test]
28925    fn parser_recognises_bare_analyze() {
28926        let s = parse("ANALYZE");
28927        assert!(matches!(s, Statement::Analyze(None)));
28928    }
28929
28930    #[test]
28931    fn parser_recognises_analyze_with_table() {
28932        let s = parse("ANALYZE users");
28933        let Statement::Analyze(Some(name)) = s else {
28934            panic!("expected Analyze, got {s:?}")
28935        };
28936        assert_eq!(name, "users");
28937    }
28938
28939    #[test]
28940    fn parser_recognises_analyze_with_quoted_table() {
28941        let s = parse("ANALYZE \"Mixed Case\"");
28942        let Statement::Analyze(Some(name)) = s else {
28943            panic!()
28944        };
28945        assert_eq!(name, "Mixed Case");
28946    }
28947
28948    #[test]
28949    fn parser_rejects_analyze_with_garbage_token() {
28950        let err = parse_statement("ANALYZE 42").expect_err("must error");
28951        assert!(!err.message.is_empty());
28952    }
28953
28954    #[test]
28955    fn analyze_display_roundtrips() {
28956        for sql in ["ANALYZE", "ANALYZE users"] {
28957            let s = parse(sql);
28958            let printed = s.to_string();
28959            let again = parse_statement(&printed)
28960                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28961            assert_eq!(s, again);
28962        }
28963    }
28964
28965    #[test]
28966    fn wait_for_display_roundtrips() {
28967        for sql in [
28968            "WAIT FOR WAL POSITION 12345",
28969            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28970        ] {
28971            let s = parse(sql);
28972            let printed = s.to_string();
28973            let again = parse_statement(&printed)
28974                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28975            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28976        }
28977    }
28978
28979    #[test]
28980    fn subscription_ddl_display_roundtrips() {
28981        for sql in [
28982            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28983            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28984            "DROP SUBSCRIPTION sub_a",
28985            "SHOW SUBSCRIPTIONS",
28986        ] {
28987            let s = parse(sql);
28988            let printed = s.to_string();
28989            let again = parse_statement(&printed)
28990                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28991            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28992        }
28993    }
28994
28995    #[test]
28996    fn parser_drop_dispatches_user_vs_publication() {
28997        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28998        // tokenises DROP. Both targets must still parse.
28999        let s = parse("DROP USER 'alice'");
29000        let Statement::DropUser { name, .. } = s else {
29001            panic!("expected DropUser, got {s:?}")
29002        };
29003        assert_eq!(name, "alice");
29004        // And DROP PUBLICATION lands the new variant.
29005        let s = parse("DROP PUBLICATION p1");
29006        assert!(matches!(s, Statement::DropPublication { .. }));
29007    }
29008
29009    #[test]
29010    fn publication_ddl_display_roundtrips() {
29011        // Every CREATE PUBLICATION variant must Display → parse →
29012        // same AST. v6.1.3 covers all three scope shapes.
29013        for sql in [
29014            "CREATE PUBLICATION pub_a",
29015            "CREATE PUBLICATION pub_a FOR ALL TABLES",
29016            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29017            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29018            "DROP PUBLICATION pub_a",
29019            "SHOW PUBLICATIONS",
29020        ] {
29021            let s = parse(sql);
29022            let printed = s.to_string();
29023            let again = parse_statement(&printed)
29024                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29025            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29026        }
29027    }
29028
29029    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29030
29031    #[test]
29032    fn create_function_returns_trigger_plpgsql_minimal() {
29033        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29034        let s = parse(sql);
29035        let Statement::CreateFunction(f) = s else {
29036            panic!("expected CreateFunction");
29037        };
29038        assert_eq!(f.name, "noop");
29039        assert!(!f.or_replace);
29040        assert!(f.args.is_empty());
29041        assert!(matches!(f.returns, FunctionReturn::Trigger));
29042        assert_eq!(f.language, "plpgsql");
29043        let FunctionBody::PlPgSql(block) = f.body else {
29044            panic!("expected PlPgSql body");
29045        };
29046        assert_eq!(block.statements.len(), 1);
29047        assert!(matches!(
29048            block.statements[0],
29049            PlPgSqlStmt::Return(ReturnTarget::New)
29050        ));
29051    }
29052
29053    #[test]
29054    fn create_function_or_replace_with_assignment() {
29055        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29056        // RETURN NEW.
29057        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29058BEGIN
29059  NEW.search_vector := to_tsvector('english', NEW.subject);
29060  RETURN NEW;
29061END;
29062$$";
29063        let s = parse(sql);
29064        let Statement::CreateFunction(f) = s else {
29065            panic!("expected CreateFunction");
29066        };
29067        assert!(f.or_replace);
29068        let FunctionBody::PlPgSql(block) = &f.body else {
29069            panic!("expected PlPgSql body");
29070        };
29071        assert_eq!(block.statements.len(), 2);
29072        // First statement: NEW.search_vector := to_tsvector(...)
29073        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29074            panic!("expected Assign as first stmt");
29075        };
29076        match target {
29077            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29078            other => panic!("expected NEW.col, got {other:?}"),
29079        }
29080        // Second statement: RETURN NEW
29081        assert!(matches!(
29082            block.statements[1],
29083            PlPgSqlStmt::Return(ReturnTarget::New)
29084        ));
29085    }
29086
29087    #[test]
29088    fn create_trigger_after_insert_or_update() {
29089        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29090        let s = parse(sql);
29091        let Statement::CreateTrigger(t) = s else {
29092            panic!("expected CreateTrigger");
29093        };
29094        assert_eq!(t.name, "tg");
29095        assert_eq!(t.table, "messages");
29096        assert_eq!(t.timing, TriggerTiming::After);
29097        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29098        assert_eq!(t.for_each, TriggerForEach::Row);
29099        assert_eq!(t.function, "update_sv");
29100    }
29101
29102    #[test]
29103    fn create_trigger_before_delete_execute_procedure_alias() {
29104        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29105        let sql =
29106            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29107        let s = parse(sql);
29108        let Statement::CreateTrigger(t) = s else {
29109            panic!("expected CreateTrigger");
29110        };
29111        assert_eq!(t.timing, TriggerTiming::Before);
29112        assert_eq!(t.events, vec![TriggerEvent::Delete]);
29113    }
29114
29115    #[test]
29116    fn drop_trigger_if_exists_round_trips() {
29117        // No parser support for DROP TRIGGER yet — added in v7.12.5
29118        // alongside the broader DROP …{IF EXISTS} cleanup. The
29119        // AST + Display impls are in place so we round-trip via
29120        // construction:
29121        let s = Statement::DropTrigger {
29122            name: "tg".into(),
29123            table: "messages".into(),
29124            if_exists: true,
29125        };
29126        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29127    }
29128
29129    #[test]
29130    fn trigger_ddl_display_roundtrips_through_parser() {
29131        // CREATE TRIGGER + its referenced CREATE FUNCTION must
29132        // Display → parse → same AST (modulo PL/pgSQL body
29133        // formatting which is parser-canonicalised).
29134        for sql in [
29135            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29136            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29137        ] {
29138            let s = parse(sql);
29139            let printed = s.to_string();
29140            let again = parse_statement(&printed)
29141                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29142            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29143        }
29144    }
29145}