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, ColumnDef, ColumnName, ColumnTypeName,
22    CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23    CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, Expr, ExtractField,
24    FkAction, ForeignKeyConstraint, FrameBound, FrameKind, FromClause, FromJoin, FunctionArg,
25    FunctionArgMode, FunctionArgType, FunctionBody, FunctionReturn, IndexMethod, InsertStatement,
26    JoinKind, Literal, NullTreatment, OrderBy, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
27    PublicationScope, RaiseLevel, ReturnTarget, SelectItem, SelectStatement, Statement, TableRef,
28    TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding, WindowFrame,
29};
30use crate::lexer::{self, LexError, Token};
31
32/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
33/// in CREATE INDEX. SPG's HNSW already routes by query operator;
34/// the opclass is accepted for `pg_dump` compatibility (mailrs
35/// migration follow-up G5).
36/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
37/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
38/// doesn't change index behaviour based on them.
39fn is_vector_opclass_name(name: &str) -> bool {
40    let lc = name.to_ascii_lowercase();
41    matches!(
42        lc.as_str(),
43        "vector_cosine_ops"
44            | "vector_l2_ops"
45            | "vector_ip_ops"
46            | "halfvec_cosine_ops"
47            | "halfvec_l2_ops"
48            | "halfvec_ip_ops"
49            | "sq8_cosine_ops"
50            | "sq8_l2_ops"
51            | "sq8_ip_ops"
52            // pg_trgm — trigram operator class. SPG's GIN index
53            // already uses tsvector tokens; trigram-style LIKE
54            // pattern matching still routes through a sequential
55            // scan, but the opclass name is accepted so PG schemas
56            // load.
57            | "gin_trgm_ops"
58            | "gist_trgm_ops"
59            // PG built-in btree opclasses occasionally appear in
60            // pg_dump output for column types with multiple
61            // sort orders (text_pattern_ops, varchar_pattern_ops,
62            // bpchar_pattern_ops).
63            | "text_pattern_ops"
64            | "varchar_pattern_ops"
65            | "bpchar_pattern_ops"
66            | "int4_ops"
67            | "int8_ops"
68            | "text_ops"
69    )
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ParseError {
74    pub message: String,
75    /// Index into the token stream where parsing tripped. Not a byte offset.
76    pub token_pos: usize,
77}
78
79impl fmt::Display for ParseError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(
82            f,
83            "parse error at token #{}: {}",
84            self.token_pos, self.message
85        )
86    }
87}
88
89impl From<LexError> for ParseError {
90    fn from(e: LexError) -> Self {
91        Self {
92            message: format!("lex: {e}"),
93            token_pos: 0,
94        }
95    }
96}
97
98/// v7.9.30 — parse a single expression (no trailing junk). Used by
99/// the engine to re-hydrate stored partial-index / unique-index
100/// predicates from their canonical Display form. The same Pratt
101/// parser the statement path uses; this entry point just skips the
102/// statement dispatch.
103pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
104    let tokens = lexer::tokenize(input)?;
105    let mut p = Parser::new(tokens);
106    let expr = p.parse_expr(0)?;
107    p.expect_eof()?;
108    Ok(expr)
109}
110
111/// Parse exactly one statement, swallow an optional trailing `;`, and require
112/// the token stream to end there.
113pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
114    let tokens = lexer::tokenize(input)?;
115    let mut p = Parser::new(tokens);
116    let stmt = p.parse_one_statement()?;
117    if matches!(p.peek(), Token::Semicolon) {
118        p.advance();
119    }
120    p.expect_eof()?;
121    Ok(stmt)
122}
123
124struct Parser {
125    tokens: Vec<Token>,
126    pos: usize,
127}
128
129impl Parser {
130    fn new(tokens: Vec<Token>) -> Self {
131        Self { tokens, pos: 0 }
132    }
133
134    fn peek(&self) -> &Token {
135        // tokens always ends with Eof; pos is clamped in advance().
136        &self.tokens[self.pos]
137    }
138
139    fn advance(&mut self) -> Token {
140        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
141        if self.pos + 1 < self.tokens.len() {
142            self.pos += 1;
143        }
144        t
145    }
146
147    fn err(&self, message: String) -> ParseError {
148        ParseError {
149            message,
150            token_pos: self.pos,
151        }
152    }
153
154    fn expect_eof(&self) -> Result<(), ParseError> {
155        if matches!(self.peek(), Token::Eof) {
156            Ok(())
157        } else {
158            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
159        }
160    }
161
162    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
163        match self.advance() {
164            Token::Ident(s) | Token::QuotedIdent(s) => Ok(s),
165            other => Err(ParseError {
166                message: format!("expected identifier, got {other:?}"),
167                token_pos: self.pos.saturating_sub(1),
168            }),
169        }
170    }
171
172    #[allow(clippy::too_many_lines)]
173    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
174        match self.peek() {
175            Token::Select => self.parse_select_stmt(),
176            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. PG-only;
177            // SPG has no PL/pgSQL so the body is consumed (lexer
178            // already turned it into a Token::String) and the whole
179            // DO statement returns CommandOk no-op. mailrs H1 +
180            // pg_dump compat.
181            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
182                self.advance();
183                // Body — single string token (dollar-quoted or
184                // ordinary).
185                match self.advance() {
186                    Token::String(_) => {}
187                    other => {
188                        return Err(self.err(alloc::format!(
189                            "expected dollar-quoted body after DO, got {other:?}"
190                        )));
191                    }
192                }
193                // Optional `LANGUAGE <name>` trailer (idents only).
194                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
195                    self.advance();
196                    let _ = self.expect_ident_like()?;
197                }
198                Ok(Statement::DoBlock)
199            }
200            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
201            // WITH isn't a reserved token in our lexer — comes through
202            // as `Token::Ident("with")` (case-insensitive).
203            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
204                self.advance();
205                self.parse_with_cte_then_select()
206            }
207            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
208            // an identifier — not a reserved keyword.
209            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
210                self.advance();
211                let mut analyze = false;
212                let mut suggest = false;
213                // v6.8.3 — `EXPLAIN (SUGGEST)` opt-in.
214                if matches!(self.peek(), Token::LParen) {
215                    self.advance();
216                    let opt = match self.peek().clone() {
217                        Token::Ident(s) | Token::QuotedIdent(s) => s,
218                        other => {
219                            return Err(self.err(format!(
220                                "expected option keyword inside EXPLAIN (…), got {other:?}"
221                            )));
222                        }
223                    };
224                    if !opt.eq_ignore_ascii_case("suggest") {
225                        return Err(self.err(format!(
226                            "unknown EXPLAIN option {opt:?}; v6.8.3 supports SUGGEST"
227                        )));
228                    }
229                    self.advance();
230                    if !matches!(self.peek(), Token::RParen) {
231                        return Err(self.err(format!(
232                            "expected ')' after EXPLAIN option, got {:?}",
233                            self.peek()
234                        )));
235                    }
236                    self.advance();
237                    suggest = true;
238                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
239                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
240                {
241                    self.advance();
242                    analyze = true;
243                }
244                let inner = self.parse_select_stmt()?;
245                let Statement::Select(s) = inner else {
246                    return Err(self.err(format!("EXPLAIN body must be a SELECT, got {inner:?}")));
247                };
248                Ok(Statement::Explain(crate::ast::ExplainStatement {
249                    analyze,
250                    inner: Box::new(s),
251                    suggest,
252                }))
253            }
254            Token::Create => self.parse_create_stmt(),
255            Token::Insert => self.parse_insert_stmt(),
256            Token::Begin => {
257                self.advance();
258                Ok(Statement::Begin)
259            }
260            Token::Commit => {
261                self.advance();
262                Ok(Statement::Commit)
263            }
264            Token::Rollback => {
265                self.advance();
266                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
267                // savepoint without ending the transaction. Bare
268                // `ROLLBACK` drops the whole TX.
269                if matches!(self.peek(), Token::To) {
270                    self.advance();
271                    if matches!(self.peek(), Token::Savepoint) {
272                        self.advance();
273                    }
274                    let name = self.expect_ident_like()?;
275                    Ok(Statement::RollbackToSavepoint(name))
276                } else {
277                    Ok(Statement::Rollback)
278                }
279            }
280            Token::Savepoint => {
281                self.advance();
282                let name = self.expect_ident_like()?;
283                Ok(Statement::Savepoint(name))
284            }
285            Token::Release => {
286                self.advance();
287                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
288                // is optional in standard SQL.
289                if matches!(self.peek(), Token::Savepoint) {
290                    self.advance();
291                }
292                let name = self.expect_ident_like()?;
293                Ok(Statement::ReleaseSavepoint(name))
294            }
295            Token::Show => {
296                self.advance();
297                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
298                // v6.1.2 promoted TABLES to a reserved keyword (for
299                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
300                // arrives as `Token::Tables` rather than a bare ident.
301                // USERS / COLUMNS remain bare idents.
302                let target = match self.advance() {
303                    Token::Tables => "tables".to_string(),
304                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
305                    other => {
306                        return Err(self.err(format!(
307                            "expected SHOW target, got {other:?}"
308                        )));
309                    }
310                };
311                match target.as_str() {
312                    "tables" => Ok(Statement::ShowTables),
313                    "users" => Ok(Statement::ShowUsers),
314                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
315                    // keyword on its own; it lands here as a bare
316                    // ident. Returning all publications + their
317                    // scope summary.
318                    "publications" => Ok(Statement::ShowPublications),
319                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
320                    "subscriptions" => Ok(Statement::ShowSubscriptions),
321                    "columns" => {
322                        if !matches!(self.peek(), Token::From) {
323                            return Err(self.err(format!(
324                                "expected FROM after SHOW COLUMNS, got {:?}",
325                                self.peek()
326                            )));
327                        }
328                        self.advance();
329                        let table = self.expect_ident_like()?;
330                        Ok(Statement::ShowColumns(table))
331                    }
332                    other => Err(self.err(format!(
333                        "unknown SHOW target {other:?}; supported: TABLES, COLUMNS, USERS, PUBLICATIONS"
334                    ))),
335                }
336            }
337            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
338            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
339            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
340            // arrived as a bare ident; tokenising it dedicatedly
341            // keeps the dispatch tree small.
342            Token::Drop => {
343                self.advance();
344                match self.peek() {
345                    Token::Publication => {
346                        self.advance();
347                        let name = self.expect_ident_or_string()?;
348                        Ok(Statement::DropPublication(name))
349                    }
350                    Token::Subscription => {
351                        self.advance();
352                        let name = self.expect_ident_or_string()?;
353                        Ok(Statement::DropSubscription(name))
354                    }
355                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
356                        self.advance();
357                        let name = self.expect_ident_or_string()?;
358                        Ok(Statement::DropUser(name))
359                    }
360                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
361                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
362                        self.advance();
363                        let if_exists = self.consume_if_exists();
364                        let name = self.expect_ident_like()?;
365                        // ON <table>
366                        if !matches!(self.peek(), Token::On) {
367                            return Err(self.err(alloc::format!(
368                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
369                                self.peek()
370                            )));
371                        }
372                        self.advance();
373                        let table = self.expect_ident_like()?;
374                        Ok(Statement::DropTrigger {
375                            name,
376                            table,
377                            if_exists,
378                        })
379                    }
380                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
381                    // v7.12.4 ignores any optional arg-list (signature-
382                    // based overload disambiguation lands in v7.12.5+).
383                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
384                        self.advance();
385                        let if_exists = self.consume_if_exists();
386                        let name = self.expect_ident_like()?;
387                        // Optional `()` — consume + discard.
388                        if matches!(self.peek(), Token::LParen) {
389                            self.advance();
390                            // Skip until matching RParen, accepting any tokens (typed args we don't model yet).
391                            let mut depth = 1usize;
392                            while depth > 0 {
393                                match self.peek() {
394                                    Token::LParen => depth += 1,
395                                    Token::RParen => depth -= 1,
396                                    Token::Eof => {
397                                        return Err(self.err(alloc::format!(
398                                            "unterminated arg list in DROP FUNCTION {name:?}"
399                                        )));
400                                    }
401                                    _ => {}
402                                }
403                                self.advance();
404                            }
405                        }
406                        Ok(Statement::DropFunction { name, if_exists })
407                    }
408                    other => Err(self.err(format!(
409                        "expected USER / PUBLICATION / SUBSCRIPTION / TRIGGER / FUNCTION after DROP, got {other:?}"
410                    ))),
411                }
412            }
413            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
414                self.advance();
415                self.parse_update_after_keyword()
416            }
417            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
418                self.advance();
419                self.parse_delete_after_keyword()
420            }
421            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
422            // ALTER is not a reserved keyword in the lexer — handled
423            // as a bare ident here.
424            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
425                self.advance();
426                self.parse_alter_after_keyword()
427            }
428            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
429            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
430            // additions needed.
431            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
432                self.advance();
433                self.parse_wait_after_keyword()
434            }
435            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
436            // Bare ANALYZE → analyse every user table; ANALYZE
437            // <name> → re-stats one. The argument is an optional
438            // ident (or quoted ident); anything else is a parse
439            // error.
440            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
441            // `WHERE` filter (carved out per V6_7_DESIGN.md
442            // STABILITY). Lex order: identifier "compact" → "cold"
443            // → "segments". Anything else after `COMPACT` is a
444            // parse error.
445            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
446                self.advance();
447                let next = self.peek().clone();
448                let cold = match next {
449                    Token::Ident(s) | Token::QuotedIdent(s) => s,
450                    _ => {
451                        return Err(
452                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
453                        );
454                    }
455                };
456                if !cold.eq_ignore_ascii_case("cold") {
457                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
458                }
459                self.advance();
460                let next = self.peek().clone();
461                let segments = match next {
462                    Token::Ident(s) | Token::QuotedIdent(s) => s,
463                    _ => {
464                        return Err(self.err(format!(
465                            "expected SEGMENTS after COMPACT COLD, got {:?}",
466                            self.peek()
467                        )));
468                    }
469                };
470                if !segments.eq_ignore_ascii_case("segments") {
471                    return Err(self.err(format!(
472                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
473                    )));
474                }
475                self.advance();
476                Ok(Statement::CompactColdSegments)
477            }
478            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
479                self.advance();
480                let target = match self.peek() {
481                    Token::Eof | Token::Semicolon => None,
482                    Token::Ident(_) | Token::QuotedIdent(_) => {
483                        Some(self.expect_ident_like()?)
484                    }
485                    other => {
486                        return Err(self.err(format!(
487                            "expected table name or end of statement after ANALYZE, got {other:?}"
488                        )));
489                    }
490                };
491                Ok(Statement::Analyze(target))
492            }
493            // v7.12.1 — `SET <name> [TO|=] <value>`. The
494            // `default_text_search_config` parameter is consumed
495            // by the FTS function dispatcher; other parameter
496            // names are recorded but treated as a no-op so PG
497            // dump output loads.
498            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
499                self.advance();
500                // PG allows `SET LOCAL` / `SET SESSION` qualifiers
501                // — accept and ignore.
502                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("local") || s.eq_ignore_ascii_case("session"))
503                {
504                    self.advance();
505                }
506                let name = self.parse_set_param_name()?;
507                // Accept either `=` or the bare `TO` keyword.
508                match self.peek() {
509                    Token::Eq => {
510                        self.advance();
511                    }
512                    Token::To => {
513                        self.advance();
514                    }
515                    other => {
516                        return Err(self.err(format!(
517                            "expected `=` or TO after SET {name}, got {other:?}"
518                        )));
519                    }
520                }
521                let value = self.parse_set_value()?;
522                Ok(Statement::SetParameter { name, value })
523            }
524            // v7.12.1 — `RESET <name>` / `RESET ALL`.
525            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
526                self.advance();
527                match self.peek().clone() {
528                    Token::All => {
529                        self.advance();
530                        Ok(Statement::ResetParameter(None))
531                    }
532                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
533                        self.advance();
534                        Ok(Statement::ResetParameter(None))
535                    }
536                    _ => {
537                        let name = self.parse_set_param_name()?;
538                        Ok(Statement::ResetParameter(Some(name)))
539                    }
540                }
541            }
542            other => Err(self.err(format!(
543                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
544                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
545            ))),
546        }
547    }
548
549    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
550        debug_assert!(matches!(self.peek(), Token::Create));
551        self.advance();
552        match self.peek() {
553            Token::Table => self.parse_create_table_stmt_after_create(),
554            Token::Index => self.parse_create_index_stmt_after_create(false),
555            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
556            // The `UNIQUE` modifier turns a partial index into a
557            // partial-uniqueness invariant (only rows matching the
558            // WHERE predicate are checked for duplicates). mailrs
559            // K1 (3 hits: email_templates default, calendar_events
560            // master, calendar_events instance).
561            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
562                self.advance();
563                if !matches!(self.peek(), Token::Index) {
564                    return Err(self.err(alloc::format!(
565                        "expected INDEX after CREATE UNIQUE, got {:?}",
566                        self.peek()
567                    )));
568                }
569                self.parse_create_index_stmt_after_create(true)
570            }
571            Token::Publication => {
572                self.advance();
573                self.parse_create_publication_after_keyword()
574            }
575            Token::Subscription => {
576                self.advance();
577                self.parse_create_subscription_after_keyword()
578            }
579            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
580            // USER isn't a reserved keyword — we look for the bare
581            // identifier so the lexer doesn't have to grow a token.
582            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
583                self.advance();
584                self.parse_create_user_after_keyword()
585            }
586            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
587            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
588            // no-op. mailrs follow-up F3.
589            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
590                self.advance();
591                self.parse_create_extension_after_keyword()
592            }
593            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
594            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
595            // optional; absorb it here and forward to the
596            // per-kind parsers with the flag. OR is a reserved
597            // keyword token.
598            Token::Or => {
599                self.advance();
600                let next = self.peek();
601                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
602                    return Err(self.err(alloc::format!(
603                        "expected REPLACE after CREATE OR, got {next:?}"
604                    )));
605                };
606                if !s2.eq_ignore_ascii_case("replace") {
607                    return Err(self.err(alloc::format!(
608                        "expected REPLACE after CREATE OR, got {s2:?}"
609                    )));
610                }
611                self.advance();
612                self.parse_create_function_or_trigger_after_or_replace(true)
613            }
614            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
615                self.advance();
616                self.parse_create_function_after_keyword(false)
617            }
618            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
619                self.advance();
620                self.parse_create_trigger_after_keyword(false)
621            }
622            other => Err(self.err(format!(
623                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER [OR REPLACE …] after CREATE, got {other:?}"
624            ))),
625        }
626    }
627
628    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
629    /// keyword decides whether we parse a function or trigger
630    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
631    /// PROCEDURE) — those land in later releases.
632    fn parse_create_function_or_trigger_after_or_replace(
633        &mut self,
634        or_replace: bool,
635    ) -> Result<Statement, ParseError> {
636        let tok = self.peek();
637        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
638            return Err(self.err(alloc::format!(
639                "expected FUNCTION / TRIGGER after CREATE OR REPLACE, got {tok:?}"
640            )));
641        };
642        if s.eq_ignore_ascii_case("function") {
643            self.advance();
644            self.parse_create_function_after_keyword(or_replace)
645        } else if s.eq_ignore_ascii_case("trigger") {
646            self.advance();
647            self.parse_create_trigger_after_keyword(or_replace)
648        } else {
649            Err(self.err(alloc::format!(
650                "expected FUNCTION / TRIGGER after CREATE OR REPLACE, got {s:?}"
651            )))
652        }
653    }
654
655    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
656    /// SPG doesn't have a registry; pgvector / similar are
657    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
658    /// the syntax lets dual-target schemas keep the line.
659    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
660        // Optional `IF NOT EXISTS`.
661        self.consume_if_not_exists();
662        let name = self.expect_ident_like()?;
663        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
664        // CASCADE / FROM '<v>' clauses; we don't model them.
665        loop {
666            match self.peek() {
667                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
668                    self.advance();
669                    continue;
670                }
671                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
672                    self.advance();
673                    let _ = self.expect_ident_like()?;
674                    continue;
675                }
676                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
677                    self.advance();
678                    // String or ident literal.
679                    let _ = self.advance();
680                    continue;
681                }
682                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
683                    self.advance();
684                    let _ = self.advance();
685                    continue;
686                }
687                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
688                    self.advance();
689                    continue;
690                }
691                _ => break,
692            }
693        }
694        Ok(Statement::CreateExtension(name))
695    }
696
697    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
698    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
699    /// already been consumed by the caller. Grammar accepted:
700    ///
701    ///   name `(` arg-list `)`
702    ///   `RETURNS` return-type
703    ///   [ `LANGUAGE` ident ]
704    ///   `AS` $$ body $$
705    ///   [ `LANGUAGE` ident ]
706    ///
707    /// Either `LANGUAGE` position is allowed; PG accepts both.
708    fn parse_create_function_after_keyword(
709        &mut self,
710        or_replace: bool,
711    ) -> Result<Statement, ParseError> {
712        let name = self.expect_ident_like()?;
713        // Argument list. v7.12.4 commonly sees the empty `()`
714        // (trigger functions); typed args parse and round-trip
715        // but the executor only invokes nullary functions.
716        if !matches!(self.peek(), Token::LParen) {
717            return Err(self.err(alloc::format!(
718                "expected '(' after function name {name:?}, got {:?}",
719                self.peek()
720            )));
721        }
722        self.advance();
723        let args = self.parse_function_arg_list()?;
724        // RETURNS clause.
725        let tok = self.peek();
726        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
727            return Err(self.err(alloc::format!(
728                "expected RETURNS after function arg list, got {tok:?}"
729            )));
730        };
731        if !s.eq_ignore_ascii_case("returns") {
732            return Err(self.err(alloc::format!(
733                "expected RETURNS after function arg list, got {s:?}"
734            )));
735        }
736        self.advance();
737        let returns = self.parse_function_return()?;
738        // Optional LANGUAGE clause (PG also accepts after AS — we'll
739        // re-check after the body too).
740        let mut language: Option<String> = self.parse_optional_language()?;
741        // `AS` followed by a $$-quoted body (lexer already
742        // collapses both `$$…$$` and `$tag$…$tag$` to a single
743        // Token::String). AS is a reserved keyword (Token::As).
744        if !matches!(self.peek(), Token::As) {
745            return Err(self.err(alloc::format!(
746                "expected AS before function body, got {:?}",
747                self.peek()
748            )));
749        }
750        self.advance();
751        let body_text = match self.peek() {
752            Token::String(s) => {
753                let body = s.clone();
754                self.advance();
755                body
756            }
757            other => {
758                return Err(self.err(alloc::format!(
759                    "expected $$-quoted function body after AS, got {other:?}"
760                )));
761            }
762        };
763        // Trailing optional LANGUAGE clause (the other PG position).
764        if language.is_none() {
765            language = self.parse_optional_language()?;
766        }
767        let language = language.unwrap_or_else(|| String::from("sql"));
768        // PL/pgSQL bodies get structure-parsed. Other languages
769        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
770        // recognise) round-trip as Raw text — the executor errors
771        // when invoked with a clear unsupported message.
772        let body = if language.eq_ignore_ascii_case("plpgsql") {
773            match parse_plpgsql_body(&body_text) {
774                Ok(block) => FunctionBody::PlPgSql(block),
775                // Best-effort: if the body parser doesn't yet
776                // support a construct used inside, fall back to
777                // raw — keeps `CREATE FUNCTION` itself working
778                // (catalogue accepts), executor errors on
779                // invocation only.
780                Err(_) => FunctionBody::Raw(body_text),
781            }
782        } else {
783            FunctionBody::Raw(body_text)
784        };
785        Ok(Statement::CreateFunction(CreateFunctionStatement {
786            name,
787            or_replace,
788            args,
789            returns,
790            language,
791            body,
792        }))
793    }
794
795    /// Closing `)`-terminated argument list. v7.12.4 commonly
796    /// sees the empty `()`; typed args round-trip but the
797    /// executor (yet) doesn't invoke them.
798    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
799        let mut args: Vec<FunctionArg> = Vec::new();
800        if matches!(self.peek(), Token::RParen) {
801            self.advance();
802            return Ok(args);
803        }
804        loop {
805            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
806            // a reserved token; OUT / INOUT are bare idents.
807            let mode = if matches!(self.peek(), Token::In) {
808                self.advance();
809                FunctionArgMode::In
810            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
811            {
812                self.advance();
813                FunctionArgMode::Out
814            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
815            {
816                self.advance();
817                FunctionArgMode::InOut
818            } else {
819                FunctionArgMode::In
820            };
821            // Optional name. The next token is either a name
822            // (followed by a type ident) or the type itself.
823            // Disambiguate by peeking ahead: if the token after
824            // the next ident is also an ident, we treat the
825            // first as the name.
826            let (name, ty_token) = {
827                let first = self.expect_ident_like()?;
828                // Peek next: if it's an ident (i.e. a type
829                // name) the `first` was the arg name.
830                match self.peek() {
831                    Token::Ident(_) | Token::QuotedIdent(_) => {
832                        let ty = self.expect_ident_like()?;
833                        (Some(first), ty)
834                    }
835                    _ => (None, first),
836                }
837            };
838            // Type — try to map to ColumnTypeName, else Raw.
839            let ty = match map_type_ident_to_column_type_name(&ty_token) {
840                Some(t) => FunctionArgType::Typed(t),
841                None => FunctionArgType::Raw(ty_token),
842            };
843            args.push(FunctionArg { mode, name, ty });
844            match self.peek() {
845                Token::Comma => {
846                    self.advance();
847                    continue;
848                }
849                Token::RParen => {
850                    self.advance();
851                    return Ok(args);
852                }
853                other => {
854                    return Err(self.err(alloc::format!(
855                        "expected , or ) in function arg list, got {other:?}"
856                    )));
857                }
858            }
859        }
860    }
861
862    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
863        let ident = self.expect_ident_like()?;
864        if ident.eq_ignore_ascii_case("trigger") {
865            return Ok(FunctionReturn::Trigger);
866        }
867        if ident.eq_ignore_ascii_case("void") {
868            return Ok(FunctionReturn::Void);
869        }
870        match map_type_ident_to_column_type_name(&ident) {
871            Some(t) => Ok(FunctionReturn::Type(t)),
872            None => Ok(FunctionReturn::Other(ident)),
873        }
874    }
875
876    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
877        match self.peek() {
878            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
879                self.advance();
880                let lang = self.expect_ident_like()?;
881                Ok(Some(lang.to_ascii_lowercase()))
882            }
883            _ => Ok(None),
884        }
885    }
886
887    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
888    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
889    /// been consumed.
890    fn parse_create_trigger_after_keyword(
891        &mut self,
892        or_replace: bool,
893    ) -> Result<Statement, ParseError> {
894        let name = self.expect_ident_like()?;
895        let timing = {
896            let ident = self.expect_ident_like()?;
897            if ident.eq_ignore_ascii_case("before") {
898                TriggerTiming::Before
899            } else if ident.eq_ignore_ascii_case("after") {
900                TriggerTiming::After
901            } else if ident.eq_ignore_ascii_case("instead") {
902                let next = self.expect_ident_like()?;
903                if !next.eq_ignore_ascii_case("of") {
904                    return Err(self.err(alloc::format!(
905                        "expected OF after INSTEAD in trigger timing, got {next:?}"
906                    )));
907                }
908                TriggerTiming::InsteadOf
909            } else {
910                return Err(self.err(alloc::format!(
911                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
912                )));
913            }
914        };
915        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
916        // OR is a reserved keyword token (Token::Or), not an Ident.
917        // v7.13.0 — after an UPDATE event we may optionally see
918        // `OF col, col, …` (mailrs round-5 G7). Columns are
919        // captured into `update_columns` once across the whole
920        // events list; multiple `UPDATE OF` clauses are rejected.
921        let mut events: Vec<TriggerEvent> = Vec::new();
922        let mut update_columns: Vec<String> = Vec::new();
923        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
924        events.push(first_ev);
925        if !first_cols.is_empty() {
926            update_columns = first_cols;
927        }
928        while matches!(self.peek(), Token::Or) {
929            self.advance();
930            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
931            events.push(ev);
932            if !cols.is_empty() {
933                if !update_columns.is_empty() {
934                    return Err(self.err(
935                        "CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into(),
936                    ));
937                }
938                update_columns = cols;
939            }
940        }
941        // ON <table>
942        let tok = self.peek();
943        let Token::On = tok else {
944            return Err(self.err(alloc::format!(
945                "expected ON after trigger events, got {tok:?}"
946            )));
947        };
948        self.advance();
949        let table = self.expect_ident_like()?;
950        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
951        // keyword (Token::For); EACH / ROW / STATEMENT are bare
952        // idents.
953        if !matches!(self.peek(), Token::For) {
954            return Err(self.err(alloc::format!(
955                "expected FOR EACH ROW / STATEMENT, got {:?}",
956                self.peek()
957            )));
958        }
959        self.advance();
960        let for_each = {
961            let e = self.expect_ident_like()?;
962            if !e.eq_ignore_ascii_case("each") {
963                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
964            }
965            let unit = self.expect_ident_like()?;
966            if unit.eq_ignore_ascii_case("row") {
967                TriggerForEach::Row
968            } else if unit.eq_ignore_ascii_case("statement") {
969                TriggerForEach::Statement
970            } else {
971                return Err(self.err(alloc::format!(
972                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
973                )));
974            }
975        };
976        // EXECUTE FUNCTION/PROCEDURE name(...)
977        let exec = self.expect_ident_like()?;
978        if !exec.eq_ignore_ascii_case("execute") {
979            return Err(self.err(alloc::format!(
980                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
981            )));
982        }
983        let fn_or_proc = self.expect_ident_like()?;
984        if !(fn_or_proc.eq_ignore_ascii_case("function")
985            || fn_or_proc.eq_ignore_ascii_case("procedure"))
986        {
987            return Err(self.err(alloc::format!(
988                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
989            )));
990        }
991        let function = self.expect_ident_like()?;
992        // Optional empty arg list `()`.
993        if matches!(self.peek(), Token::LParen) {
994            self.advance();
995            if !matches!(self.peek(), Token::RParen) {
996                return Err(self.err(alloc::format!(
997                    "v7.12.4 trigger function calls take no args; got {:?}",
998                    self.peek()
999                )));
1000            }
1001            self.advance();
1002        }
1003        Ok(Statement::CreateTrigger(CreateTriggerStatement {
1004            name,
1005            or_replace,
1006            timing,
1007            events,
1008            table,
1009            for_each,
1010            function,
1011            update_columns,
1012        }))
1013    }
1014
1015    /// v7.13.0 — parse one trigger event, then optionally consume
1016    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
1017    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
1018    fn parse_trigger_event_with_optional_of(
1019        &mut self,
1020    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
1021        let ev = self.parse_trigger_event()?;
1022        if !matches!(ev, TriggerEvent::Update) {
1023            return Ok((ev, Vec::new()));
1024        }
1025        // `OF` is a bare ident.
1026        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
1027            return Ok((ev, Vec::new()));
1028        }
1029        self.advance(); // OF
1030        let mut cols: Vec<String> = Vec::new();
1031        loop {
1032            cols.push(self.expect_ident_like()?);
1033            if matches!(self.peek(), Token::Comma) {
1034                self.advance();
1035                continue;
1036            }
1037            break;
1038        }
1039        if cols.is_empty() {
1040            return Err(self.err(
1041                "CREATE TRIGGER: `UPDATE OF` requires at least one column name".into(),
1042            ));
1043        }
1044        Ok((ev, cols))
1045    }
1046
1047    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
1048    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
1049    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
1050    /// inside the body.
1051    /// Called by [`parse_plpgsql_body`] after the body's tokens
1052    /// have been lexed into this temporary parser.
1053    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
1054        // v7.12.6 — optional DECLARE prelude.
1055        let declarations = if matches!(
1056            self.peek(),
1057            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
1058        ) {
1059            self.advance();
1060            self.parse_plpgsql_declare_block()?
1061        } else {
1062            Vec::new()
1063        };
1064        // BEGIN keyword (PL/pgSQL — distinct from the SQL
1065        // `BEGIN` transaction-start, but we can reuse the
1066        // reserved Token::Begin since the body is a separate
1067        // lex/parse context).
1068        if !matches!(self.peek(), Token::Begin) {
1069            return Err(self.err(alloc::format!(
1070                "expected BEGIN at start of plpgsql block, got {:?}",
1071                self.peek()
1072            )));
1073        }
1074        self.advance();
1075        let statements = self.parse_plpgsql_stmt_list_until_end()?;
1076        Ok(PlPgSqlBlock {
1077            declarations,
1078            statements,
1079        })
1080    }
1081
1082    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
1083    /// prelude. Caller has already consumed `DECLARE`. We stop
1084    /// reading entries when we hit `BEGIN`.
1085    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
1086        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
1087        loop {
1088            if matches!(self.peek(), Token::Begin) {
1089                return Ok(out);
1090            }
1091            let name = self.expect_ident_like()?;
1092            let ty_token = self.expect_ident_like()?;
1093            let ty = match map_type_ident_to_column_type_name(&ty_token) {
1094                Some(t) => FunctionArgType::Typed(t),
1095                None => FunctionArgType::Raw(ty_token),
1096            };
1097            let default = match self.peek() {
1098                Token::ColonEq => {
1099                    self.advance();
1100                    Some(self.parse_expr(0)?)
1101                }
1102                Token::Eq => {
1103                    // PL/pgSQL also accepts `=` for the
1104                    // DECLARE default (PG treats them the same
1105                    // in this position).
1106                    self.advance();
1107                    Some(self.parse_expr(0)?)
1108                }
1109                _ => None,
1110            };
1111            // Mandatory `;` between declarations.
1112            if !matches!(self.peek(), Token::Semicolon) {
1113                return Err(self.err(alloc::format!(
1114                    "expected ; after DECLARE entry for {name:?}, got {:?}",
1115                    self.peek()
1116                )));
1117            }
1118            self.advance();
1119            out.push(PlPgSqlDeclare { name, ty, default });
1120        }
1121    }
1122
1123    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
1124    /// the terminating `END;` (or `END IF;` etc — handled by the
1125    /// per-construct sub-parsers). Used by both the outer block
1126    /// and the IF/ELSE branch bodies.
1127    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
1128        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
1129        loop {
1130            // Allow trailing semicolons + END.
1131            while matches!(self.peek(), Token::Semicolon) {
1132                self.advance();
1133            }
1134            // END / ELSE / ELSIF — handled by the caller.
1135            if matches!(
1136                self.peek(),
1137                Token::Ident(s) | Token::QuotedIdent(s)
1138                    if s.eq_ignore_ascii_case("end")
1139                        || s.eq_ignore_ascii_case("else")
1140                        || s.eq_ignore_ascii_case("elsif")
1141                        || s.eq_ignore_ascii_case("elseif")
1142            ) {
1143                return Ok(statements);
1144            }
1145            // Otherwise: one statement, then expect `;` or
1146            // a block-terminator keyword.
1147            let stmt = self.parse_plpgsql_stmt()?;
1148            statements.push(stmt);
1149            match self.peek() {
1150                Token::Semicolon => {
1151                    self.advance();
1152                }
1153                Token::Ident(s) | Token::QuotedIdent(s)
1154                    if s.eq_ignore_ascii_case("end")
1155                        || s.eq_ignore_ascii_case("else")
1156                        || s.eq_ignore_ascii_case("elsif")
1157                        || s.eq_ignore_ascii_case("elseif") =>
1158                {
1159                    // Final statement of the block without `;`.
1160                }
1161                other => {
1162                    return Err(self.err(alloc::format!(
1163                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
1164                    )));
1165                }
1166            }
1167        }
1168    }
1169
1170    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
1171        // RETURN keyword?
1172        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
1173        {
1174            self.advance();
1175            return self.parse_plpgsql_return();
1176        }
1177        // v7.12.6 — IF block.
1178        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
1179        {
1180            self.advance();
1181            return self.parse_plpgsql_if();
1182        }
1183        // v7.12.6 — RAISE.
1184        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
1185        {
1186            self.advance();
1187            return self.parse_plpgsql_raise();
1188        }
1189        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
1190        // SELECT can appear directly inside a trigger body; we
1191        // recurse into the regular Statement parser, which will
1192        // stop at the trailing `;` (which our caller then
1193        // consumes).
1194        if matches!(self.peek(), Token::Insert)
1195            || matches!(self.peek(), Token::Select)
1196            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("delete"))
1197        {
1198            let stmt = self.parse_one_statement()?;
1199            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
1200        }
1201        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
1202        // followed by `:=` and an expression.
1203        let target = self.parse_plpgsql_assign_target()?;
1204        // PL/pgSQL assignment uses `:=`. The lexer represents
1205        // this as a colon followed by `=`; check both shapes.
1206        match self.peek() {
1207            Token::ColonEq => {
1208                self.advance();
1209            }
1210            Token::Colon => {
1211                self.advance();
1212                if !matches!(self.peek(), Token::Eq) {
1213                    return Err(self.err(alloc::format!(
1214                        "expected := after plpgsql assign target, got `:` then {:?}",
1215                        self.peek()
1216                    )));
1217                }
1218                self.advance();
1219            }
1220            other => {
1221                return Err(self.err(alloc::format!(
1222                    "expected := after plpgsql assign target, got {other:?}"
1223                )));
1224            }
1225        }
1226        let value = self.parse_expr(0)?;
1227        Ok(PlPgSqlStmt::Assign { target, value })
1228    }
1229
1230    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
1231    /// [ELSE body] END IF`. `IF` keyword already consumed.
1232    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
1233        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
1234        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
1235        loop {
1236            // <expr> THEN
1237            let cond = self.parse_expr(0)?;
1238            let then_kw = self.expect_ident_like()?;
1239            if !then_kw.eq_ignore_ascii_case("then") {
1240                return Err(self.err(alloc::format!(
1241                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
1242                )));
1243            }
1244            let body = self.parse_plpgsql_stmt_list_until_end()?;
1245            branches.push((cond, body));
1246            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
1247            match self.peek() {
1248                Token::Ident(s) | Token::QuotedIdent(s)
1249                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
1250                {
1251                    self.advance();
1252                    continue;
1253                }
1254                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
1255                    self.advance();
1256                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
1257                    break;
1258                }
1259                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
1260                    break;
1261                }
1262                other => {
1263                    return Err(self.err(alloc::format!(
1264                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
1265                    )));
1266                }
1267            }
1268        }
1269        // Expect `END IF` (the END keyword is the one we're
1270        // looking at right now).
1271        let end_kw = self.expect_ident_like()?;
1272        if !end_kw.eq_ignore_ascii_case("end") {
1273            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
1274        }
1275        let if_kw = self.expect_ident_like()?;
1276        if !if_kw.eq_ignore_ascii_case("if") {
1277            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
1278        }
1279        Ok(PlPgSqlStmt::If {
1280            branches,
1281            else_branch,
1282        })
1283    }
1284
1285    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
1286    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
1287    /// is already consumed.
1288    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
1289        let lvl_ident = self.expect_ident_like()?;
1290        let level = match lvl_ident.to_ascii_lowercase().as_str() {
1291            "notice" => RaiseLevel::Notice,
1292            "warning" => RaiseLevel::Warning,
1293            "info" => RaiseLevel::Info,
1294            "log" => RaiseLevel::Log,
1295            "debug" => RaiseLevel::Debug,
1296            "exception" => RaiseLevel::Exception,
1297            other => {
1298                return Err(self.err(alloc::format!(
1299                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
1300                )));
1301            }
1302        };
1303        // Message: required for v7.12.6. PG accepts a bare
1304        // RAISE-rethrow form (no message), reserved for future
1305        // RAISE-no-args support.
1306        let Token::String(msg) = self.peek() else {
1307            return Err(self.err(alloc::format!(
1308                "expected RAISE message string, got {:?}",
1309                self.peek()
1310            )));
1311        };
1312        let message = msg.clone();
1313        self.advance();
1314        // Optional comma-separated args (PG `%` format substitution).
1315        let mut args: Vec<Expr> = Vec::new();
1316        while matches!(self.peek(), Token::Comma) {
1317            self.advance();
1318            args.push(self.parse_expr(0)?);
1319        }
1320        Ok(PlPgSqlStmt::Raise {
1321            level,
1322            message,
1323            args,
1324        })
1325    }
1326
1327    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
1328        let head = self.expect_ident_like()?;
1329        if matches!(self.peek(), Token::Dot) {
1330            self.advance();
1331            let col = self.expect_ident_like()?;
1332            if head.eq_ignore_ascii_case("new") {
1333                return Ok(AssignTarget::NewColumn(col));
1334            }
1335            if head.eq_ignore_ascii_case("old") {
1336                return Ok(AssignTarget::OldColumn(col));
1337            }
1338            return Err(self.err(alloc::format!(
1339                "v7.12.4 plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
1340                 got {head:?}.<col>"
1341            )));
1342        }
1343        Ok(AssignTarget::Local(head))
1344    }
1345
1346    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
1347        // RETURN NEW / OLD / NULL — bare-ident forms.
1348        match self.peek() {
1349            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
1350                self.advance();
1351                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
1352            }
1353            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
1354                self.advance();
1355                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
1356            }
1357            Token::Null => {
1358                self.advance();
1359                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
1360            }
1361            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
1362            // per PL/pgSQL convention.
1363            Token::Semicolon => {
1364                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
1365            }
1366            _ => {}
1367        }
1368        // Fall through: parse a full expression.
1369        let e = self.parse_expr(0)?;
1370        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
1371    }
1372
1373    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
1374        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
1375        // are ident-shaped (the parser keys off case-insensitive
1376        // match — same shape used by the top-level Update / Delete
1377        // dispatchers at parse_one_statement).
1378        if matches!(self.peek(), Token::Insert) {
1379            self.advance();
1380            return Ok(TriggerEvent::Insert);
1381        }
1382        match self.peek() {
1383            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
1384                self.advance();
1385                Ok(TriggerEvent::Update)
1386            }
1387            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
1388                self.advance();
1389                Ok(TriggerEvent::Delete)
1390            }
1391            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
1392                self.advance();
1393                Ok(TriggerEvent::Truncate)
1394            }
1395            other => Err(self.err(alloc::format!(
1396                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
1397            ))),
1398        }
1399    }
1400
1401    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
1402    ///   - (no clause) → implicit `FOR ALL TABLES`
1403    ///   - `FOR ALL TABLES`
1404    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
1405    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
1406    ///     accepted (PG accepts both forms in PG 19).
1407    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
1408        let name = self.expect_ident_or_string()?;
1409        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
1410        // shape so existing publications keep parsing identically.
1411        let scope = if matches!(self.peek(), Token::For) {
1412            self.advance();
1413            if matches!(self.peek(), Token::All) {
1414                self.advance();
1415                if !matches!(self.peek(), Token::Tables) {
1416                    return Err(self.err(format!(
1417                        "expected TABLES after FOR ALL, got {:?}",
1418                        self.peek()
1419                    )));
1420                }
1421                self.advance();
1422                if matches!(self.peek(), Token::Except) {
1423                    self.advance();
1424                    let tables = self.parse_publication_table_list()?;
1425                    PublicationScope::AllTablesExcept(tables)
1426                } else {
1427                    PublicationScope::AllTables
1428                }
1429            } else if matches!(self.peek(), Token::Table | Token::Tables) {
1430                // PG 19 accepts both `FOR TABLE …` (singular) and
1431                // `FOR TABLES …` (plural); SPG matches.
1432                self.advance();
1433                let tables = self.parse_publication_table_list()?;
1434                PublicationScope::ForTables(tables)
1435            } else {
1436                return Err(self.err(format!(
1437                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
1438                    self.peek()
1439                )));
1440            }
1441        } else {
1442            PublicationScope::AllTables
1443        };
1444        Ok(Statement::CreatePublication(CreatePublicationStatement {
1445            name,
1446            scope,
1447        }))
1448    }
1449
1450    /// v6.1.3 — Comma-separated identifier list for the publication
1451    /// FOR-clause. Requires at least one entry; empty list is a
1452    /// parse error (PG behaviour). Quoted idents are accepted; the
1453    /// names round-trip through `Display` as `quote_ident(name)`.
1454    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
1455        let first = self.expect_ident_like()?;
1456        let mut out = alloc::vec![first];
1457        while matches!(self.peek(), Token::Comma) {
1458            self.advance();
1459            out.push(self.expect_ident_like()?);
1460        }
1461        Ok(out)
1462    }
1463
1464    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
1465    ///                 CONNECTION '<conn>'
1466    ///                 PUBLICATION <pub> [, <pub> ...]`.
1467    ///
1468    /// The clause order is fixed (CONNECTION first, then
1469    /// PUBLICATION) to match PG. No WITH-options accepted in
1470    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
1471    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
1472        let name = self.expect_ident_or_string()?;
1473        if !matches!(self.peek(), Token::Connection) {
1474            return Err(self.err(format!(
1475                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
1476                self.peek()
1477            )));
1478        }
1479        self.advance();
1480        let conn_str = self.expect_string_literal()?;
1481        if !matches!(self.peek(), Token::Publication) {
1482            return Err(self.err(format!(
1483                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
1484                self.peek()
1485            )));
1486        }
1487        self.advance();
1488        // Reuse the publication FOR-list parser shape: at least one
1489        // identifier, comma-separated.
1490        let first = self.expect_ident_like()?;
1491        let mut publications = alloc::vec![first];
1492        while matches!(self.peek(), Token::Comma) {
1493            self.advance();
1494            publications.push(self.expect_ident_like()?);
1495        }
1496        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
1497            name,
1498            conn_str,
1499            publications,
1500        }))
1501    }
1502
1503    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
1504    /// All keywords after `WAIT` are bare idents in v6.1.x; no
1505    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
1506    /// that fit `u64`.
1507    /// v7.12.1 — parameter name in `SET <name>` may be dotted
1508    /// (`pg_catalog.default_text_search_config` etc).
1509    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
1510        let mut name = self.expect_ident_like()?;
1511        while matches!(self.peek(), Token::Dot) {
1512            self.advance();
1513            let next = self.expect_ident_like()?;
1514            name.push('.');
1515            name.push_str(&next);
1516        }
1517        Ok(name.to_ascii_lowercase())
1518    }
1519
1520    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
1521        match self.advance() {
1522            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
1523            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
1524                Ok(crate::ast::SetValue::Default)
1525            }
1526            Token::Ident(s) | Token::QuotedIdent(s) => {
1527                let mut accum = s;
1528                while matches!(self.peek(), Token::Dot) {
1529                    self.advance();
1530                    let next = self.expect_ident_like()?;
1531                    accum.push('.');
1532                    accum.push_str(&next);
1533                }
1534                Ok(crate::ast::SetValue::Ident(accum))
1535            }
1536            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
1537            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
1538            other => Err(self.err(format!(
1539                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
1540            ))),
1541        }
1542    }
1543
1544    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
1545        // FOR is a v6.1.2-reserved keyword (Token::For). The
1546        // other two are bare idents — they've never needed lexer
1547        // support and we keep it that way.
1548        if !matches!(self.peek(), Token::For) {
1549            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
1550        }
1551        self.advance();
1552        self.expect_keyword_ident("wal")?;
1553        self.expect_keyword_ident("position")?;
1554        let pos = self.expect_u64_literal()?;
1555        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
1556        {
1557            self.advance();
1558            self.expect_keyword_ident("timeout")?;
1559            Some(self.expect_u64_literal()?)
1560        } else {
1561            None
1562        };
1563        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
1564    }
1565
1566    /// v6.1.7 helper — consume a `Token::Integer` and check it
1567    /// fits `u64`. WAL positions and millisecond timeouts are
1568    /// non-negative.
1569    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
1570        match self.advance() {
1571            Token::Integer(n) if n >= 0 => Ok(n as u64),
1572            Token::Integer(n) => Err(ParseError {
1573                message: format!("expected non-negative integer, got {n}"),
1574                token_pos: self.pos.saturating_sub(1),
1575            }),
1576            other => Err(ParseError {
1577                message: format!("expected integer literal, got {other:?}"),
1578                token_pos: self.pos.saturating_sub(1),
1579            }),
1580        }
1581    }
1582
1583    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
1584    /// ROLE '<role>' (defaults to readonly). All string slots accept
1585    /// either a quoted ident or a quoted string literal.
1586    fn parse_create_user_after_keyword(&mut self) -> Result<Statement, ParseError> {
1587        let name = self.expect_ident_or_string()?;
1588        self.expect_keyword_ident("with")?;
1589        self.expect_keyword_ident("password")?;
1590        let password = self.expect_string_literal()?;
1591        let role = if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
1592            && s.eq_ignore_ascii_case("role")
1593        {
1594            self.advance();
1595            self.expect_string_literal()?
1596        } else {
1597            "readonly".to_string()
1598        };
1599        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
1600            name,
1601            password,
1602            role,
1603        }))
1604    }
1605
1606    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
1607    /// Caller already consumed the leading `UPDATE` ident.
1608    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
1609        let table = self.expect_ident_like()?;
1610        self.expect_keyword_ident("set")?;
1611        let mut assignments = Vec::new();
1612        loop {
1613            let col = self.expect_ident_like()?;
1614            if !matches!(self.peek(), Token::Eq) {
1615                return Err(self.err(format!(
1616                    "expected `=` after column name in UPDATE SET, got {:?}",
1617                    self.peek()
1618                )));
1619            }
1620            self.advance();
1621            let value = self.parse_expr(0)?;
1622            assignments.push((col, value));
1623            if matches!(self.peek(), Token::Comma) {
1624                self.advance();
1625                continue;
1626            }
1627            break;
1628        }
1629        let where_ = if matches!(self.peek(), Token::Where) {
1630            self.advance();
1631            Some(self.parse_expr(0)?)
1632        } else {
1633            None
1634        };
1635        let returning = self.parse_optional_returning()?;
1636        Ok(Statement::Update(crate::ast::UpdateStatement {
1637            table,
1638            assignments,
1639            where_,
1640            returning,
1641        }))
1642    }
1643
1644    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
1645    /// the leading `DELETE` ident.
1646    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
1647        if !matches!(self.peek(), Token::From) {
1648            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
1649        }
1650        self.advance();
1651        let table = self.expect_ident_like()?;
1652        let where_ = if matches!(self.peek(), Token::Where) {
1653            self.advance();
1654            Some(self.parse_expr(0)?)
1655        } else {
1656            None
1657        };
1658        let returning = self.parse_optional_returning()?;
1659        Ok(Statement::Delete(crate::ast::DeleteStatement {
1660            table,
1661            where_,
1662            returning,
1663        }))
1664    }
1665
1666    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
1667    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
1668    /// as SELECT, so `RETURNING *`, `RETURNING col`,
1669    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
1670    fn parse_optional_returning(
1671        &mut self,
1672    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
1673        let is_returning_kw = matches!(
1674            self.peek(),
1675            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
1676        );
1677        if !is_returning_kw {
1678            return Ok(None);
1679        }
1680        self.advance();
1681        let mut items = Vec::new();
1682        loop {
1683            items.push(self.parse_select_item()?);
1684            if matches!(self.peek(), Token::Comma) {
1685                self.advance();
1686                continue;
1687            }
1688            break;
1689        }
1690        Ok(Some(items))
1691    }
1692
1693    /// v6.0.4 — parse the tail of an ALTER statement after the
1694    /// leading `ALTER` keyword has been consumed. Only one form is
1695    /// supported in v6.0.4:
1696    ///
1697    /// ```text
1698    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
1699    /// ```
1700    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
1701        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
1702        match self.advance() {
1703            Token::Index => {}
1704            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
1705            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
1706            Token::Table => return self.parse_alter_table_after_keyword(),
1707            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
1708                return self.parse_alter_table_after_keyword();
1709            }
1710            other => {
1711                return Err(self.err(format!(
1712                    "expected INDEX or TABLE after ALTER, got {other:?}"
1713                )));
1714            }
1715        }
1716        let name = self.expect_ident_like()?;
1717        // REBUILD
1718        self.expect_keyword_ident("rebuild")?;
1719        // Optional: WITH (encoding = <enc>)
1720        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
1721            self.advance();
1722            if !matches!(self.peek(), Token::LParen) {
1723                return Err(self.err(format!(
1724                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
1725                    self.peek()
1726                )));
1727            }
1728            self.advance();
1729            self.expect_keyword_ident("encoding")?;
1730            if !matches!(self.peek(), Token::Eq) {
1731                return Err(self.err(format!(
1732                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
1733                    self.peek()
1734                )));
1735            }
1736            self.advance();
1737            let enc_ident = match self.advance() {
1738                Token::Ident(s) | Token::QuotedIdent(s) => s,
1739                other => {
1740                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
1741                }
1742            };
1743            let enc = match enc_ident.to_ascii_lowercase().as_str() {
1744                "f32" => VecEncoding::F32,
1745                "sq8" => VecEncoding::Sq8,
1746                "half" => VecEncoding::F16,
1747                other => {
1748                    return Err(self.err(format!(
1749                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
1750                    )));
1751                }
1752            };
1753            if !matches!(self.peek(), Token::RParen) {
1754                return Err(self.err(format!(
1755                    "expected ')' after encoding value, got {:?}",
1756                    self.peek()
1757                )));
1758            }
1759            self.advance();
1760            Some(enc)
1761        } else {
1762            None
1763        };
1764        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
1765            name,
1766            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
1767        }))
1768    }
1769
1770    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
1771    /// only `SET` form currently supported; future v6.7.x can add
1772    /// more SET subjects without changing the dispatch shape.
1773    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
1774    /// subactions. Single-subaction shape stays a 1-element vec.
1775    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
1776        let table_name = self.expect_ident_like()?;
1777        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
1778        loop {
1779            let subaction = self.parse_alter_table_subaction()?;
1780            // ADD COLUMN with inline REFERENCES emits both an
1781            // AddColumn and an AddForeignKey subaction; the
1782            // helper returns 1 or 2 items.
1783            targets.extend(subaction);
1784            if matches!(self.peek(), Token::Comma) {
1785                self.advance();
1786                continue;
1787            }
1788            break;
1789        }
1790        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1791            name: table_name,
1792            targets,
1793        }))
1794    }
1795
1796    /// Parse one ALTER TABLE subaction. Returns a Vec because
1797    /// inline `REFERENCES` on `ADD COLUMN` produces both an
1798    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
1799    fn parse_alter_table_subaction(
1800        &mut self,
1801    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
1802        match self.peek() {
1803            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
1804                self.advance();
1805                let setting = self.expect_ident_like()?;
1806                if !setting.eq_ignore_ascii_case("hot_tier_bytes") {
1807                    return Err(self.err(alloc::format!(
1808                        "ALTER TABLE SET: unknown setting {setting:?}; supported: hot_tier_bytes"
1809                    )));
1810                }
1811                if !matches!(self.peek(), Token::Eq) {
1812                    return Err(self.err(alloc::format!(
1813                        "expected '=' after hot_tier_bytes, got {:?}",
1814                        self.peek()
1815                    )));
1816                }
1817                self.advance();
1818                let n = self.expect_u64_literal()?;
1819                Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)])
1820            }
1821            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
1822                self.advance();
1823                let is_fk = matches!(
1824                    self.peek(),
1825                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
1826                        || s.eq_ignore_ascii_case("foreign")
1827                );
1828                if is_fk {
1829                    let fk = self.parse_table_level_fk()?;
1830                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
1831                }
1832                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
1833                    self.advance();
1834                }
1835                let mut if_not_exists = false;
1836                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
1837                    self.advance();
1838                    if !matches!(self.peek(), Token::Not) {
1839                        return Err(self.err(alloc::format!(
1840                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
1841                            self.peek()
1842                        )));
1843                    }
1844                    self.advance();
1845                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
1846                        return Err(self.err(alloc::format!(
1847                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
1848                            self.peek()
1849                        )));
1850                    }
1851                    self.advance();
1852                    if_not_exists = true;
1853                }
1854                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
1855                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
1856                // returns ColumnDef + an optional inline FK.
1857                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
1858                let col_name = column.name.clone();
1859                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
1860                    column,
1861                    if_not_exists,
1862                }];
1863                if let Some(mut fk) = col_level_fk {
1864                    if fk.columns.is_empty() {
1865                        fk.columns.push(col_name);
1866                    }
1867                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
1868                }
1869                Ok(out)
1870            }
1871            Token::Drop => {
1872                self.advance();
1873                // v7.13.3 — dispatch on the next token. mailrs round-7
1874                // S8 closed DROP COLUMN; round-6 S7 closed
1875                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
1876                // RESTRICT modifiers.
1877                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
1878                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
1879                let subject = match self.peek() {
1880                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
1881                        self.advance();
1882                        "constraint"
1883                    }
1884                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
1885                        self.advance();
1886                        "column"
1887                    }
1888                    // PG-canonical bare `DROP <col>` without COLUMN
1889                    // keyword is also valid; treat any other ident
1890                    // as the column name.
1891                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
1892                    other => {
1893                        return Err(self.err(alloc::format!(
1894                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
1895                        )));
1896                    }
1897                };
1898                let mut if_exists = false;
1899                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
1900                    let n1 = self.tokens.get(self.pos + 1);
1901                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
1902                        self.advance();
1903                        self.advance();
1904                        if_exists = true;
1905                    }
1906                }
1907                let name = self.expect_ident_like()?;
1908                let mut cascade = false;
1909                if matches!(
1910                    self.peek(),
1911                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
1912                        || s.eq_ignore_ascii_case("restrict")
1913                ) {
1914                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
1915                    {
1916                        cascade = true;
1917                    }
1918                    self.advance();
1919                }
1920                if subject == "constraint" {
1921                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
1922                        name,
1923                        if_exists,
1924                    }])
1925                } else {
1926                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
1927                        column: name,
1928                        if_exists,
1929                        cascade,
1930                    }])
1931                }
1932            }
1933            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
1934                self.advance();
1935                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
1936                    self.advance();
1937                }
1938                let col_name = self.expect_ident_like()?;
1939                match self.peek() {
1940                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
1941                        self.advance();
1942                    }
1943                    other => {
1944                        return Err(self.err(alloc::format!(
1945                            "expected TYPE after ALTER COLUMN <name>, got {other:?}"
1946                        )));
1947                    }
1948                }
1949                let new_type = self.parse_column_type_name()?;
1950                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
1951                {
1952                    self.advance();
1953                    Some(self.parse_expr(0)?)
1954                } else {
1955                    None
1956                };
1957                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
1958                    column: col_name,
1959                    new_type,
1960                    using,
1961                }])
1962            }
1963            other => Err(self.err(alloc::format!(
1964                "expected SET / ADD / DROP / ALTER in ALTER TABLE, got {other:?}"
1965            ))),
1966        }
1967    }
1968
1969    /// Consume a bare ident if its lowercase matches `kw`, else err.
1970    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
1971        match self.advance() {
1972            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
1973            other => Err(ParseError {
1974                message: format!("expected {kw:?}, got {other:?}"),
1975                token_pos: self.pos.saturating_sub(1),
1976            }),
1977        }
1978    }
1979
1980    /// Accept either a quoted identifier (`"foo"`) or a quoted string
1981    /// literal (`'foo'`) — same shape used by CREATE USER for the
1982    /// username slot.
1983    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
1984        match self.advance() {
1985            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
1986            other => Err(ParseError {
1987                message: format!("expected identifier or string, got {other:?}"),
1988                token_pos: self.pos.saturating_sub(1),
1989            }),
1990        }
1991    }
1992
1993    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
1994        match self.advance() {
1995            Token::String(s) => Ok(s),
1996            other => Err(ParseError {
1997                message: format!("expected quoted string, got {other:?}"),
1998                token_pos: self.pos.saturating_sub(1),
1999            }),
2000        }
2001    }
2002
2003    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
2004        // Caller dispatches on Token::Select; the inner helper handles
2005        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
2006        // get a fresh bare-select parse and may not have their own ORDER
2007        // BY / LIMIT.
2008        let mut head = self.parse_bare_select()?;
2009        while matches!(self.peek(), Token::Union) {
2010            self.advance();
2011            let kind = if matches!(self.peek(), Token::All) {
2012                self.advance();
2013                UnionKind::All
2014            } else {
2015                UnionKind::Distinct
2016            };
2017            let peer = self.parse_bare_select()?;
2018            head.unions.push((kind, peer));
2019        }
2020        head.order_by = if matches!(self.peek(), Token::Order) {
2021            self.advance();
2022            if !matches!(self.peek(), Token::By) {
2023                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
2024            }
2025            self.advance();
2026            // v6.4.0 — multi-key ORDER BY. Loop over comma-separated
2027            // `<expr> [ASC|DESC]` items.
2028            let mut keys = Vec::new();
2029            loop {
2030                let expr = self.parse_expr(0)?;
2031                let desc = if matches!(self.peek(), Token::Desc) {
2032                    self.advance();
2033                    true
2034                } else if matches!(self.peek(), Token::Asc) {
2035                    self.advance();
2036                    false
2037                } else {
2038                    false
2039                };
2040                keys.push(OrderBy { expr, desc });
2041                if matches!(self.peek(), Token::Comma) {
2042                    self.advance();
2043                } else {
2044                    break;
2045                }
2046            }
2047            keys
2048        } else {
2049            Vec::new()
2050        };
2051        head.limit = if matches!(self.peek(), Token::Limit) {
2052            self.advance();
2053            Some(self.parse_limit_expr("LIMIT")?)
2054        } else {
2055            None
2056        };
2057        head.offset = if matches!(self.peek(), Token::Offset) {
2058            self.advance();
2059            Some(self.parse_limit_expr("OFFSET")?)
2060        } else {
2061            None
2062        };
2063        Ok(Statement::Select(head))
2064    }
2065
2066    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
2067    /// Bind value gets resolved during prepared-statement Execute;
2068    /// the Pratt expression parser would over-accept here (e.g.
2069    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
2070    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
2071        match self.advance() {
2072            Token::Integer(n) if n >= 0 => u32::try_from(n)
2073                .map(crate::ast::LimitExpr::Literal)
2074                .map_err(|_| ParseError {
2075                    message: alloc::format!("{label} value too large: {n}"),
2076                    token_pos: self.pos.saturating_sub(1),
2077                }),
2078            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
2079            other => Err(ParseError {
2080                message: alloc::format!(
2081                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
2082                ),
2083                token_pos: self.pos.saturating_sub(1),
2084            }),
2085        }
2086    }
2087
2088    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
2089    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
2090    /// `unions` empty and `order_by` / `limit` `None`; the top-level
2091    /// `parse_select_stmt` is responsible for filling those in.
2092    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
2093        if !matches!(self.peek(), Token::Select) {
2094            return Err(self.err(format!(
2095                "expected SELECT to start a query block, got {:?}",
2096                self.peek()
2097            )));
2098        }
2099        self.advance();
2100        let distinct = if matches!(self.peek(), Token::Distinct) {
2101            self.advance();
2102            true
2103        } else {
2104            false
2105        };
2106        let items = self.parse_select_list()?;
2107        let from = if matches!(self.peek(), Token::From) {
2108            self.advance();
2109            Some(self.parse_from_clause()?)
2110        } else {
2111            None
2112        };
2113        let where_ = if matches!(self.peek(), Token::Where) {
2114            self.advance();
2115            Some(self.parse_expr(0)?)
2116        } else {
2117            None
2118        };
2119        let mut group_by_all = false;
2120        let group_by = if matches!(self.peek(), Token::Group) {
2121            self.advance();
2122            if !matches!(self.peek(), Token::By) {
2123                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
2124            }
2125            self.advance();
2126            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
2127            // every non-aggregate SELECT-list item later.
2128            if matches!(self.peek(), Token::All) {
2129                self.advance();
2130                group_by_all = true;
2131                None
2132            } else {
2133                let mut groups = Vec::new();
2134                loop {
2135                    groups.push(self.parse_expr(0)?);
2136                    if matches!(self.peek(), Token::Comma) {
2137                        self.advance();
2138                    } else {
2139                        break;
2140                    }
2141                }
2142                Some(groups)
2143            }
2144        } else {
2145            None
2146        };
2147        let having = if matches!(self.peek(), Token::Having) {
2148            self.advance();
2149            Some(self.parse_expr(0)?)
2150        } else {
2151            None
2152        };
2153        Ok(SelectStatement {
2154            ctes: Vec::new(),
2155            distinct,
2156            items,
2157            from,
2158            where_,
2159            group_by,
2160            group_by_all,
2161            having,
2162            unions: Vec::new(),
2163            order_by: Vec::new(),
2164            limit: None,
2165            offset: None,
2166        })
2167    }
2168
2169    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
2170        // Caller already consumed CREATE; we're sitting on TABLE.
2171        debug_assert!(matches!(self.peek(), Token::Table));
2172        self.advance();
2173        let if_not_exists = self.consume_if_not_exists();
2174        let name = self.expect_ident_like()?;
2175        if !matches!(self.peek(), Token::LParen) {
2176            return Err(self.err(format!(
2177                "expected '(' after table name, got {:?}",
2178                self.peek()
2179            )));
2180        }
2181        self.advance();
2182        let mut columns = Vec::new();
2183        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
2184        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
2185        loop {
2186            // v7.6.0 / v7.9.18 — distinguish table-level constraint
2187            // clauses from column definitions. Constraints start
2188            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
2189            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
2190            // a column.
2191            if self.peek_table_level_pk_start() {
2192                table_constraints.push(self.parse_table_level_primary_key()?);
2193            } else if self.peek_table_level_unique_start() {
2194                table_constraints.push(self.parse_table_level_unique()?);
2195            } else if self.peek_table_level_check_start() {
2196                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
2197                table_constraints.push(self.parse_table_level_check()?);
2198            } else if self.peek_constraint_or_fk_start() {
2199                foreign_keys.push(self.parse_table_level_fk()?);
2200            } else {
2201                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
2202                // v7.13.0 — fold inline UNIQUE / CHECK column
2203                // constraints into table-level entries so the
2204                // engine path stays uniform.
2205                if col.is_unique {
2206                    table_constraints.push(crate::ast::TableConstraint::Unique {
2207                        name: None,
2208                        columns: alloc::vec![col.name.clone()],
2209                        nulls_not_distinct: false,
2210                    });
2211                }
2212                if let Some(check_expr) = col.check.clone() {
2213                    table_constraints.push(crate::ast::TableConstraint::Check {
2214                        name: None,
2215                        expr: check_expr,
2216                    });
2217                }
2218                columns.push(col);
2219                if let Some(fk) = col_level_fk {
2220                    foreign_keys.push(fk);
2221                }
2222            }
2223            match self.peek() {
2224                Token::Comma => {
2225                    self.advance();
2226                }
2227                Token::RParen => {
2228                    self.advance();
2229                    break;
2230                }
2231                other => {
2232                    return Err(
2233                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
2234                    );
2235                }
2236            }
2237        }
2238        if columns.is_empty() {
2239            return Err(self.err("CREATE TABLE requires at least one column".into()));
2240        }
2241        Ok(Statement::CreateTable(CreateTableStatement {
2242            name,
2243            columns,
2244            if_not_exists,
2245            foreign_keys,
2246            table_constraints,
2247        }))
2248    }
2249
2250    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
2251    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
2252    /// sure (otherwise a column literally named `primary` would
2253    /// be mistaken).
2254    fn peek_table_level_pk_start(&self) -> bool {
2255        let cur = self.peek();
2256        let nxt = self.tokens.get(self.pos + 1);
2257        let nxt2 = self.tokens.get(self.pos + 2);
2258        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
2259        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
2260        let is_lparen = matches!(nxt2, Some(Token::LParen));
2261        is_primary && is_key && is_lparen
2262    }
2263
2264    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
2265    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
2266    /// (mailrs round-5 G10).
2267    fn peek_table_level_unique_start(&self) -> bool {
2268        let cur = self.peek();
2269        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
2270        if !is_unique {
2271            return false;
2272        }
2273        let n1 = self.tokens.get(self.pos + 1);
2274        // Plain `UNIQUE (…)`.
2275        if matches!(n1, Some(Token::LParen)) {
2276            return true;
2277        }
2278        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
2279        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
2280        if !is_nulls {
2281            return false;
2282        }
2283        let n2 = self.tokens.get(self.pos + 2);
2284        let n3 = self.tokens.get(self.pos + 3);
2285        let n4 = self.tokens.get(self.pos + 4);
2286        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
2287        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
2288            return true;
2289        }
2290        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
2291        if matches!(n2, Some(Token::Not))
2292            && matches!(n3, Some(Token::Distinct))
2293            && matches!(n4, Some(Token::LParen))
2294        {
2295            return true;
2296        }
2297        false
2298    }
2299
2300    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2301        self.advance(); // PRIMARY
2302        self.advance(); // KEY
2303        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
2304        Ok(crate::ast::TableConstraint::PrimaryKey {
2305            name: None,
2306            columns,
2307        })
2308    }
2309
2310    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2311        self.advance(); // UNIQUE
2312        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
2313        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
2314        // is `NULLS DISTINCT` per the SQL standard.
2315        let mut nulls_not_distinct = false;
2316        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
2317            let n1 = self.tokens.get(self.pos + 1);
2318            let n2 = self.tokens.get(self.pos + 2);
2319            let is_not = matches!(n1, Some(Token::Not));
2320            let is_distinct = matches!(n2, Some(Token::Distinct));
2321            if is_not && is_distinct {
2322                self.advance(); // NULLS
2323                self.advance(); // NOT
2324                self.advance(); // DISTINCT
2325                nulls_not_distinct = true;
2326            } else if matches!(n1, Some(Token::Distinct)) {
2327                self.advance(); // NULLS
2328                self.advance(); // DISTINCT
2329            }
2330        }
2331        let columns = self.parse_paren_ident_list("UNIQUE")?;
2332        Ok(crate::ast::TableConstraint::Unique {
2333            name: None,
2334            columns,
2335            nulls_not_distinct,
2336        })
2337    }
2338
2339    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
2340    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
2341    /// expression.
2342    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2343        self.advance(); // CHECK
2344        if !matches!(self.peek(), Token::LParen) {
2345            return Err(self.err(alloc::format!(
2346                "expected '(' after CHECK, got {:?}",
2347                self.peek()
2348            )));
2349        }
2350        self.advance();
2351        let expr = self.parse_expr(0)?;
2352        if !matches!(self.peek(), Token::RParen) {
2353            return Err(self.err(alloc::format!(
2354                "expected ')' to close CHECK predicate, got {:?}",
2355                self.peek()
2356            )));
2357        }
2358        self.advance();
2359        Ok(crate::ast::TableConstraint::Check { name: None, expr })
2360    }
2361
2362    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
2363    fn peek_table_level_check_start(&self) -> bool {
2364        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
2365    }
2366
2367    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
2368        if !matches!(self.peek(), Token::LParen) {
2369            return Err(self.err(alloc::format!(
2370                "expected '(' after {ctx}, got {:?}",
2371                self.peek()
2372            )));
2373        }
2374        self.advance();
2375        let mut out = Vec::new();
2376        loop {
2377            out.push(self.expect_ident_like()?);
2378            match self.peek() {
2379                Token::Comma => {
2380                    self.advance();
2381                }
2382                Token::RParen => {
2383                    self.advance();
2384                    break;
2385                }
2386                other => {
2387                    return Err(self.err(alloc::format!(
2388                        "expected ',' or ')' in {ctx} list, got {other:?}"
2389                    )));
2390                }
2391            }
2392        }
2393        if out.is_empty() {
2394            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
2395        }
2396        Ok(out)
2397    }
2398
2399    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
2400    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
2401    /// table-level FK; a column def never starts with either keyword
2402    /// (column names are not in this reserved set).
2403    fn peek_constraint_or_fk_start(&self) -> bool {
2404        let is_constraint_kw = matches!(
2405            self.peek(),
2406            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
2407        );
2408        let is_foreign_kw = matches!(
2409            self.peek(),
2410            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
2411        );
2412        is_constraint_kw || is_foreign_kw
2413    }
2414
2415    /// v7.6.0 — parse a table-level FK clause:
2416    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
2417    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
2418    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
2419        let mut name: Option<String> = None;
2420        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
2421            self.advance();
2422            name = Some(self.expect_ident_like()?);
2423        }
2424        // `FOREIGN`
2425        match self.advance() {
2426            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
2427            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
2428        }
2429        // `KEY`
2430        match self.advance() {
2431            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
2432            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
2433        }
2434        // `(col, col, ...)`
2435        if !matches!(self.peek(), Token::LParen) {
2436            return Err(self.err(format!(
2437                "expected '(' after FOREIGN KEY, got {:?}",
2438                self.peek()
2439            )));
2440        }
2441        self.advance();
2442        let mut columns = Vec::new();
2443        loop {
2444            columns.push(self.expect_ident_like()?);
2445            match self.peek() {
2446                Token::Comma => {
2447                    self.advance();
2448                }
2449                Token::RParen => {
2450                    self.advance();
2451                    break;
2452                }
2453                other => {
2454                    return Err(self.err(format!(
2455                        "expected ',' or ')' in FK column list, got {other:?}"
2456                    )));
2457                }
2458            }
2459        }
2460        if columns.is_empty() {
2461            return Err(self.err("FOREIGN KEY requires at least one column".into()));
2462        }
2463        let (parent_table, parent_columns, on_delete, on_update) =
2464            self.parse_references_tail(columns.len())?;
2465        Ok(ForeignKeyConstraint {
2466            name,
2467            columns,
2468            parent_table,
2469            parent_columns,
2470            on_delete,
2471            on_update,
2472        })
2473    }
2474
2475    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
2476    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
2477    /// the local column count, used to default the parent column
2478    /// list when omitted (SQL spec: parent's PK is implied).
2479    fn parse_references_tail(
2480        &mut self,
2481        expected_arity: usize,
2482    ) -> Result<(String, Vec<String>, FkAction, FkAction), ParseError> {
2483        match self.advance() {
2484            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
2485            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
2486        }
2487        let parent_table = self.expect_ident_like()?;
2488        let mut parent_columns: Vec<String> = Vec::new();
2489        if matches!(self.peek(), Token::LParen) {
2490            self.advance();
2491            loop {
2492                parent_columns.push(self.expect_ident_like()?);
2493                match self.peek() {
2494                    Token::Comma => {
2495                        self.advance();
2496                    }
2497                    Token::RParen => {
2498                        self.advance();
2499                        break;
2500                    }
2501                    other => {
2502                        return Err(self.err(format!(
2503                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
2504                        )));
2505                    }
2506                }
2507            }
2508        }
2509        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
2510            return Err(self.err(format!(
2511                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
2512                expected_arity,
2513                parent_columns.len()
2514            )));
2515        }
2516        // v7.6.7 — accept and reject `[NOT] DEFERRABLE [INITIALLY
2517        // {DEFERRED | IMMEDIATE}]` so existing PG dumps don't fail
2518        // at parse time. SPG's single-writer model has no deferred
2519        // constraint window, so we surface this as a clean
2520        // unsupported-feature error rather than a syntax error.
2521        loop {
2522            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
2523                return Err(self.err(
2524                    "DEFERRABLE constraints are not supported (SPG is single-writer; \
2525                     constraints are always evaluated immediately at commit)"
2526                        .into(),
2527                ));
2528            }
2529            if matches!(self.peek(), Token::Not) {
2530                let look = self.tokens.get(self.pos + 1);
2531                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
2532                    // NOT DEFERRABLE — accept as the SPG default
2533                    // and consume both tokens silently.
2534                    self.advance();
2535                    self.advance();
2536                    // Optional `INITIALLY IMMEDIATE` clause.
2537                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially"))
2538                    {
2539                        self.advance();
2540                        match self.advance() {
2541                            Token::Ident(s) if s.eq_ignore_ascii_case("immediate") => {}
2542                            other => {
2543                                return Err(self.err(format!(
2544                                    "expected IMMEDIATE after INITIALLY for NOT DEFERRABLE, \
2545                                     got {other:?}"
2546                                )));
2547                            }
2548                        }
2549                    }
2550                    continue;
2551                }
2552                break;
2553            }
2554            break;
2555        }
2556        // Optional `ON DELETE <action>` and `ON UPDATE <action>` in
2557        // either order, each at most once.
2558        let mut on_delete = FkAction::Restrict;
2559        let mut on_update = FkAction::Restrict;
2560        let mut seen_on_delete = false;
2561        let mut seen_on_update = false;
2562        loop {
2563            if !matches!(self.peek(), Token::On) {
2564                break;
2565            }
2566            self.advance();
2567            let which = self.advance();
2568            let action = self.parse_fk_action()?;
2569            match which {
2570                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
2571                    if seen_on_delete {
2572                        return Err(self.err("ON DELETE specified twice".into()));
2573                    }
2574                    seen_on_delete = true;
2575                    on_delete = action;
2576                }
2577                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
2578                    if seen_on_update {
2579                        return Err(self.err("ON UPDATE specified twice".into()));
2580                    }
2581                    seen_on_update = true;
2582                    on_update = action;
2583                }
2584                other => {
2585                    return Err(
2586                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
2587                    );
2588                }
2589            }
2590        }
2591        Ok((parent_table, parent_columns, on_delete, on_update))
2592    }
2593
2594    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
2595    /// NO ACTION`.
2596    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
2597        match self.advance() {
2598            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
2599            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
2600            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
2601                Token::Null => Ok(FkAction::SetNull),
2602                Token::Default => Ok(FkAction::SetDefault),
2603                other => Err(self.err(format!(
2604                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
2605                ))),
2606            },
2607            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
2608                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
2609                other => Err(self.err(format!(
2610                    "expected ACTION after NO in FK action, got {other:?}"
2611                ))),
2612            },
2613            other => Err(self.err(format!(
2614                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
2615            ))),
2616        }
2617    }
2618
2619    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
2620    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
2621    fn consume_if_not_exists(&mut self) -> bool {
2622        // `IF` arrives as a bare Ident (we don't reserve it because it
2623        // also appears mid-expression in PG, though we don't support
2624        // those forms yet).
2625        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
2626        if !looks_like_if {
2627            return false;
2628        }
2629        // Peek one ahead before committing: only consume IF when it's
2630        // actually `IF NOT EXISTS`.
2631        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
2632            return false;
2633        }
2634        if !matches!(
2635            self.tokens.get(self.pos + 2),
2636            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
2637        ) {
2638            return false;
2639        }
2640        self.advance(); // IF
2641        self.advance(); // NOT
2642        self.advance(); // EXISTS
2643        true
2644    }
2645
2646    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
2647    /// Consumes IF EXISTS as a pair; returns false otherwise
2648    /// without consuming any tokens.
2649    fn consume_if_exists(&mut self) -> bool {
2650        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
2651        if !looks_like_if {
2652            return false;
2653        }
2654        if !matches!(
2655            self.tokens.get(self.pos + 1),
2656            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
2657        ) {
2658            return false;
2659        }
2660        self.advance(); // IF
2661        self.advance(); // EXISTS
2662        true
2663    }
2664
2665    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
2666    /// qualifiers after an index column ref. ASC / DESC are
2667    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
2668    /// We accept and discard them since single-column BTree
2669    /// stores rows in natural key order today.
2670    fn consume_optional_index_column_qualifiers(&mut self) {
2671        loop {
2672            match self.peek() {
2673                Token::Asc | Token::Desc => {
2674                    self.advance();
2675                }
2676                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
2677                    let look = self.tokens.get(self.pos + 1);
2678                    if matches!(
2679                        look,
2680                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
2681                            || k.eq_ignore_ascii_case("last")
2682                    ) {
2683                        self.advance();
2684                        self.advance();
2685                    } else {
2686                        break;
2687                    }
2688                }
2689                _ => break,
2690            }
2691        }
2692    }
2693
2694    fn parse_create_index_stmt_after_create(
2695        &mut self,
2696        is_unique: bool,
2697    ) -> Result<Statement, ParseError> {
2698        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
2699        debug_assert!(matches!(self.peek(), Token::Index));
2700        self.advance();
2701        let if_not_exists = self.consume_if_not_exists();
2702        let name = self.expect_ident_like()?;
2703        if !matches!(self.peek(), Token::On) {
2704            return Err(self.err(format!(
2705                "expected ON after CREATE INDEX <name>, got {:?}",
2706                self.peek()
2707            )));
2708        }
2709        self.advance();
2710        let table = self.expect_ident_like()?;
2711        // Optional `USING <method>` — only recognised method in v2.0 is
2712        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
2713        // ident `using` (we don't promote it to a reserved keyword
2714        // because it isn't reserved anywhere else in our SQL surface).
2715        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
2716            self.advance();
2717            let m = self.expect_ident_like()?;
2718            match m.to_ascii_lowercase().as_str() {
2719                "hnsw" => IndexMethod::Hnsw,
2720                "btree" => IndexMethod::BTree,
2721                "brin" => IndexMethod::Brin,
2722                // v7.12.3 — real GIN inverted index over `tsvector`.
2723                // v7.9.26b's `USING gin` → BTree silent fallback is
2724                // gone; the engine validates that the indexed column
2725                // is `tsvector` at CREATE INDEX time.
2726                "gin" => IndexMethod::Gin,
2727                // v7.9.26b — PG `pg_dump` emits `USING gist` /
2728                // `USING spgist` / `USING hash` for their built-in
2729                // AMs that SPG doesn't have a matching
2730                // implementation for; degrade to BTree on the
2731                // leading column so the schema loads + the index
2732                // catalogue stays consistent. Operator pays the
2733                // planner cost only for the queries that would have
2734                // used the specialised AM.
2735                "gist" | "spgist" | "hash" => IndexMethod::BTree,
2736                // v7.11.3 — pgvector ships both `ivfflat` and
2737                // `hnsw`. Customers shouldn't have to choose
2738                // their on-disk index method based on what SPG
2739                // implements; accept `ivfflat` as a synonym for
2740                // `hnsw` so PG schemas using either method drop
2741                // in. The vector distance op (`<->` / `<#>` /
2742                // `<=>`) at query time still picks the metric.
2743                "ivfflat" => IndexMethod::Hnsw,
2744                other => {
2745                    return Err(self.err(alloc::format!(
2746                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
2747                    )));
2748                }
2749            }
2750        } else {
2751            IndexMethod::BTree
2752        };
2753        if !matches!(self.peek(), Token::LParen) {
2754            return Err(self.err(format!(
2755                "expected '(' before indexed column, got {:?}",
2756                self.peek()
2757            )));
2758        }
2759        self.advance();
2760        // v6.8.2 — accept either a bare column ident (legacy) or
2761        // an expression `fn(col, …)` for expression indexes.
2762        // Distinguish by peeking the token *after* the current
2763        // ident: `ident )` is the legacy column-only path;
2764        // anything else triggers the Pratt expression parser.
2765        // (`advance()` uses `mem::replace` to nil out the current
2766        // slot, so we can't save+rewind cleanly — peek-ahead via
2767        // direct index avoids the mutation.)
2768        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
2769            // Single column with `)` immediately after — fast path.
2770            // v7.9.29 — also: bare column followed by `,` (the
2771            // multi-column form `(a, b, c)`). Without this branch
2772            // the leading ident gets pulled into `parse_expr`
2773            // which then sets `expression = Some(Column(a))` and
2774            // breaks Display round-trip on the multi-column shape.
2775            Token::Ident(s) | Token::QuotedIdent(s)
2776                if matches!(
2777                    self.tokens.get(self.pos + 1),
2778                    Some(Token::RParen | Token::Comma)
2779                ) =>
2780            {
2781                self.advance();
2782                (s, None)
2783            }
2784            // v7.9.22 — single column followed by a pgvector
2785            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
2786            // SPG's HNSW currently picks its distance metric from
2787            // the query's operator (`<->` / `<#>` / `<=>`), so the
2788            // opclass is informational — accepted and discarded.
2789            // Recognised opclasses: vector_cosine_ops, vector_l2_ops,
2790            // vector_ip_ops, halfvec_*_ops, sq8_*_ops.
2791            Token::Ident(s) | Token::QuotedIdent(s)
2792                if matches!(
2793                    self.tokens.get(self.pos + 1),
2794                    Some(Token::Ident(op) | Token::QuotedIdent(op))
2795                        if is_vector_opclass_name(op)
2796                ) =>
2797            {
2798                self.advance(); // column name
2799                self.advance(); // opclass ident — drop
2800                (s, None)
2801            }
2802            Token::Ident(_) | Token::QuotedIdent(_) => {
2803                let key_expr = self.parse_expr(0)?;
2804                let primary = extract_first_column(&key_expr).ok_or_else(|| {
2805                    self.err("expression index key must reference at least one column".into())
2806                })?;
2807                (primary, Some(key_expr))
2808            }
2809            other => {
2810                return Err(self.err(format!(
2811                    "expected column ident or expression, got {other:?}"
2812                )));
2813            }
2814        };
2815        // v7.9.14 — accept extra comma-separated columns inside
2816        // the index key parens (`CREATE INDEX … (a, b, c)`).
2817        // mailrs F2. Each extra column may carry an optional
2818        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
2819        // — parsed and discarded; SPG doesn't honour direction
2820        // on a BTree index today (column ordering is intrinsic
2821        // to the storage). v7.10 will widen to genuine composite
2822        // index keys.
2823        let mut extra_columns: Vec<String> = Vec::new();
2824        // The leading column may also have ASC/DESC after it.
2825        self.consume_optional_index_column_qualifiers();
2826        while matches!(self.peek(), Token::Comma) {
2827            self.advance();
2828            let extra = self.expect_ident_like()?;
2829            self.consume_optional_index_column_qualifiers();
2830            extra_columns.push(extra);
2831        }
2832        if !matches!(self.peek(), Token::RParen) {
2833            return Err(self.err(format!(
2834                "expected ')' after indexed column / expression, got {:?}",
2835                self.peek()
2836            )));
2837        }
2838        self.advance();
2839        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
2840        // index-only-scan annotation. Bare ident (not a reserved
2841        // keyword) so we test by case-insensitive string match.
2842        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
2843        {
2844            self.advance();
2845            if !matches!(self.peek(), Token::LParen) {
2846                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
2847            }
2848            self.advance();
2849            let mut cols = Vec::new();
2850            loop {
2851                cols.push(self.expect_ident_like()?);
2852                match self.peek() {
2853                    Token::Comma => {
2854                        self.advance();
2855                    }
2856                    Token::RParen => {
2857                        self.advance();
2858                        break;
2859                    }
2860                    other => {
2861                        return Err(self.err(format!(
2862                            "expected ',' or ')' in INCLUDE list, got {other:?}"
2863                        )));
2864                    }
2865                }
2866            }
2867            cols
2868        } else {
2869            Vec::new()
2870        };
2871        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
2872        // storage parameters. pgvector emits `WITH (lists = N)` for
2873        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
2874        // SPG's HNSW picks its own parameters today (tunable via
2875        // env vars), so the WITH clause is informational and dropped.
2876        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
2877            self.advance();
2878            if !matches!(self.peek(), Token::LParen) {
2879                return Err(self.err(format!(
2880                    "expected '(' after WITH in CREATE INDEX, got {:?}",
2881                    self.peek()
2882                )));
2883            }
2884            self.advance();
2885            loop {
2886                if matches!(self.peek(), Token::RParen) {
2887                    self.advance();
2888                    break;
2889                }
2890                // Drain `key = value` or bare `key` tokens.
2891                let _ = self.advance(); // key
2892                if matches!(self.peek(), Token::Eq) {
2893                    self.advance();
2894                    let _ = self.advance(); // value (int / string / ident)
2895                }
2896                match self.peek() {
2897                    Token::Comma => {
2898                        self.advance();
2899                    }
2900                    Token::RParen => {
2901                        self.advance();
2902                        break;
2903                    }
2904                    other => {
2905                        return Err(self.err(format!(
2906                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
2907                        )));
2908                    }
2909                }
2910            }
2911        }
2912        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
2913        let partial_predicate = if matches!(self.peek(), Token::Where) {
2914            self.advance();
2915            Some(self.parse_expr(0)?)
2916        } else {
2917            None
2918        };
2919        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
2920        // sense: uniqueness over an ANN structure has no clean
2921        // semantics. Reject early. (BRIN UNIQUE is similarly
2922        // meaningless — block both.)
2923        if is_unique && !matches!(method, IndexMethod::BTree) {
2924            return Err(self.err(alloc::format!(
2925                "UNIQUE is only supported on BTree indexes, got USING {:?}",
2926                method
2927            )));
2928        }
2929        Ok(Statement::CreateIndex(CreateIndexStatement {
2930            name,
2931            table,
2932            column,
2933            method,
2934            if_not_exists,
2935            included_columns,
2936            partial_predicate,
2937            extra_columns: extra_columns.clone(),
2938            expression,
2939            is_unique,
2940        }))
2941    }
2942
2943    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
2944    /// column-level `REFERENCES ...` clause. The trailing FK is
2945    /// normalised into table-level shape (single-element columns +
2946    /// parent_columns) so the engine sees one uniform constraint list.
2947    fn parse_column_def_with_fk(
2948        &mut self,
2949    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
2950        let col = self.parse_column_def()?;
2951        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
2952        let inline_references = matches!(
2953            self.peek(),
2954            Token::Ident(s) if s.eq_ignore_ascii_case("references")
2955        );
2956        if !inline_references {
2957            return Ok((col, None));
2958        }
2959        let (parent_table, parent_columns, on_delete, on_update) = self.parse_references_tail(1)?;
2960        let fk = ForeignKeyConstraint {
2961            name: None,
2962            columns: vec![col.name.clone()],
2963            parent_table,
2964            parent_columns,
2965            on_delete,
2966            on_update,
2967        };
2968        Ok((col, Some(fk)))
2969    }
2970
2971    /// v7.13.0 — parse a column type (consuming the type ident and
2972    /// any trailing parameters / `[]`), without surrounding column
2973    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
2974    /// Returns the resolved `ColumnTypeName` plus implied
2975    /// `(auto_increment, not_null)` flags from PG SERIAL family
2976    /// shorthands — callers that don't expect those (ALTER COLUMN
2977    /// TYPE) can discard them.
2978    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
2979        let (ty, _, _) = self.parse_type_with_implied_flags()?;
2980        Ok(ty)
2981    }
2982
2983    fn parse_type_with_implied_flags(
2984        &mut self,
2985    ) -> Result<(ColumnTypeName, bool, bool), ParseError> {
2986        let ty_ident = match self.advance() {
2987            Token::Ident(s) => s,
2988            other => {
2989                return Err(ParseError {
2990                    message: format!("expected column type, got {other:?}"),
2991                    token_pos: self.pos.saturating_sub(1),
2992                });
2993            }
2994        };
2995        let mut implied_auto_increment = false;
2996        let mut implied_not_null = false;
2997        let mut ty = match ty_ident.as_str() {
2998            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
2999            "smallserial" | "serial2" => {
3000                implied_auto_increment = true;
3001                implied_not_null = true;
3002                ColumnTypeName::SmallInt
3003            }
3004            "serial" | "serial4" => {
3005                implied_auto_increment = true;
3006                implied_not_null = true;
3007                ColumnTypeName::Int
3008            }
3009            "bigserial" | "serial8" => {
3010                implied_auto_increment = true;
3011                implied_not_null = true;
3012                ColumnTypeName::BigInt
3013            }
3014            // MySQL flavours we accept by aliasing to the closest SPG
3015            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
3016            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
3017            // 24-bit) → INT. UNSIGNED modifiers are consumed below
3018            // without semantic effect.
3019            "smallint" | "tinyint" => ColumnTypeName::SmallInt,
3020            // INTEGER is MySQL's spelling for INT; MEDIUMINT widens up.
3021            "int" | "integer" | "mediumint" => ColumnTypeName::Int,
3022            "bigint" => ColumnTypeName::BigInt,
3023            // DOUBLE / REAL are 64-bit IEEE — same as our FLOAT.
3024            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
3025            // (mailrs round-5 G6). Consume the optional `PRECISION`
3026            // tail when the type keyword was `double` / `DOUBLE`.
3027            "float" | "double" | "real" => {
3028                if ty_ident.eq_ignore_ascii_case("double")
3029                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
3030                {
3031                    self.advance();
3032                }
3033                ColumnTypeName::Float
3034            }
3035            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
3036            "float4" | "float8" => ColumnTypeName::Float,
3037            "text" => ColumnTypeName::Text,
3038            "bool" | "boolean" => ColumnTypeName::Bool,
3039            "varchar" => ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?),
3040            "char" => ColumnTypeName::Char(self.parse_paren_size("CHAR")?),
3041            "vector" => {
3042                let dim = self.parse_paren_size("VECTOR")?;
3043                let encoding = self.parse_optional_vector_encoding()?;
3044                ColumnTypeName::Vector { dim, encoding }
3045            }
3046            "numeric" => {
3047                let (precision, scale) = self.parse_optional_numeric_params()?;
3048                ColumnTypeName::Numeric(precision, scale)
3049            }
3050            "date" => ColumnTypeName::Date,
3051            // MySQL's `DATETIME` is the same domain as standard
3052            // `TIMESTAMP` — accept both spellings.
3053            "timestamp" | "datetime" => ColumnTypeName::Timestamp,
3054            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
3055            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
3056            // only PG-wire OID differs.
3057            "timestamptz" => ColumnTypeName::Timestamptz,
3058            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
3059            // validation. We accept the JSONB spelling too because
3060            // most PG clients default to it; SPG doesn't distinguish
3061            // the two (no path-operator perf advantage to model).
3062            "json" => ColumnTypeName::Json,
3063            "jsonb" => ColumnTypeName::Jsonb,
3064            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
3065            // surface here. Same storage shape; mapping happens at
3066            // the engine side via the ColumnTypeName → DataType
3067            // resolver. Literal forms are handled at coerce_value
3068            // time so the lexer stays untouched.
3069            "bytea" | "bytes" => ColumnTypeName::Bytes,
3070            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
3071            // The actual `to_tsvector` / `@@` / `ts_rank` surface
3072            // arrives in v7.12.1+; the type itself loads here so
3073            // mailrs's `scripts/init-schema.sql` runs unmodified.
3074            "tsvector" => ColumnTypeName::TsVector,
3075            "tsquery" => ColumnTypeName::TsQuery,
3076            other => {
3077                return Err(ParseError {
3078                    message: format!("unsupported column type {other:?}"),
3079                    token_pos: self.pos.saturating_sub(1),
3080                });
3081            }
3082        };
3083        // MySQL's `UNSIGNED` modifier sits right after the type
3084        // keyword. SPG doesn't carry a separate unsigned variant —
3085        // accepting the keyword keeps existing schemas compatible
3086        // without changing semantics. Drop it silently.
3087        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned")) {
3088            self.advance();
3089        }
3090        // v7.10.10 — postfix `[]` widens TEXT → TEXT[]. PG accepts
3091        // `TYPE[]` after any base type; v7.10 only models TEXT[]
3092        // so we reject other base types here. mailrs uses TEXT[]
3093        // for labels / addresses / message-on-thread.
3094        if matches!(self.peek(), Token::LBracket) {
3095            self.advance();
3096            if !matches!(self.peek(), Token::RBracket) {
3097                return Err(self.err(alloc::format!(
3098                    "TEXT[] takes no dimension; got {:?}",
3099                    self.peek()
3100                )));
3101            }
3102            self.advance();
3103            // v7.11.13 — widened to INT[] and BIGINT[] in addition
3104            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
3105            // still error here.
3106            ty = match ty {
3107                ColumnTypeName::Text => ColumnTypeName::TextArray,
3108                ColumnTypeName::Int => ColumnTypeName::IntArray,
3109                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
3110                other => {
3111                    return Err(self.err(alloc::format!(
3112                        "v7.11 supports TEXT[] / INT[] / BIGINT[] only; got {other:?}[]"
3113                    )));
3114                }
3115            };
3116        }
3117        Ok((ty, implied_auto_increment, implied_not_null))
3118    }
3119
3120    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
3121        let name = self.expect_ident_like()?;
3122        let (ty, implied_auto_increment, implied_not_null) =
3123            self.parse_type_with_implied_flags()?;
3124        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
3125        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
3126        // each at most once.
3127        let mut default: Option<Expr> = None;
3128        let mut nullable = !implied_not_null;
3129        let mut nullability_seen = implied_not_null;
3130        let mut auto_increment = implied_auto_increment;
3131        let mut is_primary_key = false;
3132        let mut is_unique = false;
3133        let mut check: Option<Expr> = None;
3134        loop {
3135            if matches!(self.peek(), Token::Default) {
3136                if default.is_some() {
3137                    return Err(self.err("DEFAULT specified twice".into()));
3138                }
3139                self.advance();
3140                default = Some(self.parse_expr(0)?);
3141                continue;
3142            }
3143            if matches!(self.peek(), Token::Not) {
3144                if nullability_seen {
3145                    return Err(self.err("NOT NULL specified twice".into()));
3146                }
3147                self.advance();
3148                if !matches!(self.peek(), Token::Null) {
3149                    return Err(self.err(format!(
3150                        "expected NULL after NOT in column def, got {:?}",
3151                        self.peek()
3152                    )));
3153                }
3154                self.advance();
3155                nullable = false;
3156                nullability_seen = true;
3157                continue;
3158            }
3159            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
3160            // arrives as a bare Ident. Match either, case-insensitive.
3161            if let Token::Ident(s) = self.peek()
3162                && (s.eq_ignore_ascii_case("auto_increment")
3163                    || s.eq_ignore_ascii_case("autoincrement"))
3164            {
3165                if auto_increment {
3166                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
3167                }
3168                self.advance();
3169                auto_increment = true;
3170                continue;
3171            }
3172            // v7.9.13 — inline `PRIMARY KEY` column constraint
3173            // (mailrs F1). Implies `NOT NULL`. The engine creates
3174            // a BTree index for the PK column at CREATE TABLE time
3175            // so FK parent-side index lookups resolve.
3176            if let Token::Ident(s) = self.peek()
3177                && s.eq_ignore_ascii_case("primary")
3178            {
3179                if is_primary_key {
3180                    return Err(self.err("PRIMARY KEY specified twice".into()));
3181                }
3182                // Peek-ahead for the required `KEY` token.
3183                let next = self.tokens.get(self.pos + 1);
3184                let next_is_key = matches!(
3185                    next,
3186                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
3187                );
3188                if !next_is_key {
3189                    return Err(self.err(format!(
3190                        "expected KEY after PRIMARY in column def, got {:?}",
3191                        next
3192                    )));
3193                }
3194                self.advance(); // PRIMARY
3195                self.advance(); // KEY
3196                is_primary_key = true;
3197                if nullability_seen && nullable {
3198                    return Err(self.err(
3199                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
3200                    ));
3201                }
3202                nullable = false;
3203                nullability_seen = true;
3204                continue;
3205            }
3206            // v7.13.0 — inline `UNIQUE` column constraint
3207            // (mailrs round-5 G2). Fold into a single-column
3208            // table-level UNIQUE at CREATE TABLE post-process time.
3209            if let Token::Ident(s) = self.peek()
3210                && s.eq_ignore_ascii_case("unique")
3211            {
3212                if is_unique {
3213                    return Err(self.err("UNIQUE specified twice".into()));
3214                }
3215                self.advance();
3216                is_unique = true;
3217                continue;
3218            }
3219            // v7.13.0 — inline `CHECK (<expr>)` column constraint
3220            // (mailrs round-5 G3). PG semantics: column-level
3221            // CHECK is equivalent to a table-level CHECK. Multiple
3222            // inline CHECKs on the same column AND together.
3223            if let Token::Ident(s) = self.peek()
3224                && s.eq_ignore_ascii_case("check")
3225            {
3226                self.advance();
3227                if !matches!(self.peek(), Token::LParen) {
3228                    return Err(self.err(alloc::format!(
3229                        "expected '(' after CHECK in column def, got {:?}",
3230                        self.peek()
3231                    )));
3232                }
3233                self.advance();
3234                let pred = self.parse_expr(0)?;
3235                if !matches!(self.peek(), Token::RParen) {
3236                    return Err(self.err(alloc::format!(
3237                        "expected ')' to close CHECK predicate, got {:?}",
3238                        self.peek()
3239                    )));
3240                }
3241                self.advance();
3242                check = Some(match check.take() {
3243                    Some(prev) => Expr::Binary {
3244                        op: BinOp::And,
3245                        lhs: Box::new(prev),
3246                        rhs: Box::new(pred),
3247                    },
3248                    None => pred,
3249                });
3250                continue;
3251            }
3252            break;
3253        }
3254        Ok(ColumnDef {
3255            name,
3256            ty,
3257            nullable,
3258            default,
3259            auto_increment,
3260            is_primary_key,
3261            is_unique,
3262            check,
3263        })
3264    }
3265
3266    /// `NUMERIC` may appear without parameters, with one (precision
3267    /// only, scale=0), or with both. Returns `(precision, scale)` with
3268    /// 0 = unspecified for the bare form.
3269    fn parse_optional_numeric_params(&mut self) -> Result<(u8, u8), ParseError> {
3270        if !matches!(self.peek(), Token::LParen) {
3271            // Bare `NUMERIC` — PG treats this as "unlimited precision";
3272            // we surface it as precision=0 to mean "unconstrained" so
3273            // the engine doesn't need a separate variant.
3274            return Ok((0, 0));
3275        }
3276        self.advance();
3277        let precision = match self.advance() {
3278            Token::Integer(n) if (1..=38).contains(&n) => u8::try_from(n).expect("range-checked"),
3279            other => {
3280                return Err(ParseError {
3281                    message: format!(
3282                        "NUMERIC precision must be an integer in 1..=38, got {other:?}"
3283                    ),
3284                    token_pos: self.pos.saturating_sub(1),
3285                });
3286            }
3287        };
3288        let scale = if matches!(self.peek(), Token::Comma) {
3289            self.advance();
3290            match self.advance() {
3291                Token::Integer(n) if (0..=i64::from(precision)).contains(&n) => {
3292                    u8::try_from(n).expect("range-checked")
3293                }
3294                other => {
3295                    return Err(ParseError {
3296                        message: format!(
3297                            "NUMERIC scale must be a non-negative integer ≤ precision, got {other:?}"
3298                        ),
3299                        token_pos: self.pos.saturating_sub(1),
3300                    });
3301                }
3302            }
3303        } else {
3304            0
3305        };
3306        if !matches!(self.peek(), Token::RParen) {
3307            return Err(self.err(format!(
3308                "expected ')' to close NUMERIC params, got {:?}",
3309                self.peek()
3310            )));
3311        }
3312        self.advance();
3313        Ok((precision, scale))
3314    }
3315
3316    /// Parse `(N)` where `N` is a positive integer literal — used by the
3317    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
3318    /// for the error message.
3319    /// v6.0.1: parse the optional `USING <encoding>` clause that
3320    /// follows `VECTOR(N)` in a column definition. Missing clause
3321    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
3322    /// ident → `ParseError` listing the encodings recognised today.
3323    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
3324        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
3325            return Ok(VecEncoding::F32);
3326        }
3327        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
3328        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
3329        // consume the token when the very next token is a known
3330        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
3331        // `USING` for the caller — it's the rewrite-expression form.
3332        let n1 = self.tokens.get(self.pos + 1);
3333        let next_is_encoding = matches!(
3334            n1,
3335            Some(Token::Ident(s))
3336                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
3337        );
3338        if !next_is_encoding {
3339            return Ok(VecEncoding::F32);
3340        }
3341        self.advance();
3342        let enc_ident = match self.advance() {
3343            Token::Ident(s) => s,
3344            other => {
3345                return Err(self.err(format!(
3346                    "expected vector encoding after USING, got {other:?}"
3347                )));
3348            }
3349        };
3350        match enc_ident.to_ascii_lowercase().as_str() {
3351            "sq8" => Ok(VecEncoding::Sq8),
3352            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
3353            // binary16 per-element storage.
3354            "half" => Ok(VecEncoding::F16),
3355            other => Err(self.err(format!(
3356                "unknown vector encoding {other:?}; supported: SQ8, HALF"
3357            ))),
3358        }
3359    }
3360
3361    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
3362        if !matches!(self.peek(), Token::LParen) {
3363            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
3364        }
3365        self.advance();
3366        let n = match self.advance() {
3367            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
3368                message: format!("{label} size too large: {n}"),
3369                token_pos: self.pos.saturating_sub(1),
3370            })?,
3371            other => {
3372                return Err(ParseError {
3373                    message: format!("expected positive integer {label} size, got {other:?}"),
3374                    token_pos: self.pos.saturating_sub(1),
3375                });
3376            }
3377        };
3378        if !matches!(self.peek(), Token::RParen) {
3379            return Err(self.err(format!(
3380                "expected ')' after {label} size, got {:?}",
3381                self.peek()
3382            )));
3383        }
3384        self.advance();
3385        Ok(n)
3386    }
3387
3388    fn parse_insert_stmt(&mut self) -> Result<Statement, ParseError> {
3389        debug_assert!(matches!(self.peek(), Token::Insert));
3390        self.advance();
3391        if !matches!(self.peek(), Token::Into) {
3392            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
3393        }
3394        self.advance();
3395        let table = self.expect_ident_like()?;
3396        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
3397        let columns = if matches!(self.peek(), Token::LParen) {
3398            self.advance();
3399            let mut names = Vec::new();
3400            loop {
3401                names.push(self.expect_ident_like()?);
3402                match self.peek() {
3403                    Token::Comma => {
3404                        self.advance();
3405                    }
3406                    Token::RParen => {
3407                        self.advance();
3408                        break;
3409                    }
3410                    other => {
3411                        return Err(self.err(format!(
3412                            "expected ',' or ')' in INSERT column list, got {other:?}"
3413                        )));
3414                    }
3415                }
3416            }
3417            Some(names)
3418        } else {
3419            None
3420        };
3421        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3422        // round-5 G4). Dispatch on VALUES vs SELECT.
3423        if matches!(self.peek(), Token::Select) {
3424            let select_stmt = match self.parse_select_stmt()? {
3425                Statement::Select(s) => s,
3426                other => {
3427                    return Err(self.err(alloc::format!(
3428                        "expected SELECT after INSERT INTO ... target, got {other:?}"
3429                    )));
3430                }
3431            };
3432            let on_conflict = self.parse_optional_on_conflict()?;
3433            let returning = self.parse_optional_returning()?;
3434            return Ok(Statement::Insert(InsertStatement {
3435                table,
3436                columns,
3437                rows: Vec::new(),
3438                select_source: Some(Box::new(select_stmt)),
3439                on_conflict,
3440                returning,
3441            }));
3442        }
3443        if !matches!(self.peek(), Token::Values) {
3444            return Err(self.err(format!(
3445                "expected VALUES or SELECT after table name, got {:?}",
3446                self.peek()
3447            )));
3448        }
3449        self.advance();
3450        if !matches!(self.peek(), Token::LParen) {
3451            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
3452        }
3453        let mut rows = Vec::new();
3454        loop {
3455            // Each iteration consumes one `(expr, expr, …)` tuple.
3456            if !matches!(self.peek(), Token::LParen) {
3457                return Err(self.err(format!(
3458                    "expected '(' for next VALUES tuple, got {:?}",
3459                    self.peek()
3460                )));
3461            }
3462            self.advance();
3463            let mut tuple = Vec::new();
3464            loop {
3465                tuple.push(self.parse_expr(0)?);
3466                match self.peek() {
3467                    Token::Comma => {
3468                        self.advance();
3469                    }
3470                    Token::RParen => {
3471                        self.advance();
3472                        break;
3473                    }
3474                    other => {
3475                        return Err(self.err(format!(
3476                            "expected ',' or ')' in VALUES tuple, got {other:?}"
3477                        )));
3478                    }
3479                }
3480            }
3481            if tuple.is_empty() {
3482                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
3483            }
3484            rows.push(tuple);
3485            // Continue with comma-separated tuples.
3486            if matches!(self.peek(), Token::Comma) {
3487                self.advance();
3488            } else {
3489                break;
3490            }
3491        }
3492        let on_conflict = self.parse_optional_on_conflict()?;
3493        let returning = self.parse_optional_returning()?;
3494        Ok(Statement::Insert(InsertStatement {
3495            table,
3496            columns,
3497            rows,
3498            select_source: None,
3499            on_conflict,
3500            returning,
3501        }))
3502    }
3503
3504    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
3505    /// clause sitting between the INSERT body and the trailing
3506    /// RETURNING. All keywords come in as bare idents; `ON` is
3507    /// a reserved Token though.
3508    fn parse_optional_on_conflict(
3509        &mut self,
3510    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
3511        if !matches!(self.peek(), Token::On) {
3512            return Ok(None);
3513        }
3514        // Peek further: we want exactly "ON CONFLICT ...". If the
3515        // next ident isn't "conflict", let some other parser handle.
3516        let next_is_conflict = matches!(
3517            self.tokens.get(self.pos + 1),
3518            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
3519        );
3520        if !next_is_conflict {
3521            return Ok(None);
3522        }
3523        self.advance(); // ON
3524        self.advance(); // CONFLICT
3525        // Optional `(col [, col]*)` target list.
3526        let mut target_columns: Vec<String> = Vec::new();
3527        if matches!(self.peek(), Token::LParen) {
3528            self.advance();
3529            loop {
3530                target_columns.push(self.expect_ident_like()?);
3531                match self.peek() {
3532                    Token::Comma => {
3533                        self.advance();
3534                    }
3535                    Token::RParen => {
3536                        self.advance();
3537                        break;
3538                    }
3539                    other => {
3540                        return Err(self.err(alloc::format!(
3541                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
3542                        )));
3543                    }
3544                }
3545            }
3546        }
3547        // Required `DO`.
3548        match self.advance() {
3549            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
3550            other => {
3551                return Err(self.err(alloc::format!(
3552                    "expected DO after ON CONFLICT [(…)], got {other:?}"
3553                )));
3554            }
3555        }
3556        // Action: NOTHING | UPDATE SET …
3557        let action = match self.advance() {
3558            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
3559                crate::ast::OnConflictAction::Nothing
3560            }
3561            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3562                self.parse_on_conflict_update_action()?
3563            }
3564            other => {
3565                return Err(self.err(alloc::format!(
3566                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
3567                )));
3568            }
3569        };
3570        Ok(Some(crate::ast::OnConflictClause {
3571            target_columns,
3572            action,
3573        }))
3574    }
3575
3576    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
3577    /// `SET col = expr [, …] [WHERE cond]`. Caller already
3578    /// consumed `UPDATE`.
3579    fn parse_on_conflict_update_action(
3580        &mut self,
3581    ) -> Result<crate::ast::OnConflictAction, ParseError> {
3582        // `SET`
3583        match self.advance() {
3584            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
3585            other => {
3586                return Err(self.err(alloc::format!(
3587                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
3588                )));
3589            }
3590        }
3591        let mut assignments: Vec<(String, Expr)> = Vec::new();
3592        loop {
3593            let col = self.expect_ident_like()?;
3594            if !matches!(self.peek(), Token::Eq) {
3595                return Err(self.err(alloc::format!(
3596                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
3597                    self.peek()
3598                )));
3599            }
3600            self.advance();
3601            let value = self.parse_expr(0)?;
3602            assignments.push((col, value));
3603            if matches!(self.peek(), Token::Comma) {
3604                self.advance();
3605                continue;
3606            }
3607            break;
3608        }
3609        let where_ = if matches!(self.peek(), Token::Where) {
3610            self.advance();
3611            Some(self.parse_expr(0)?)
3612        } else {
3613            None
3614        };
3615        Ok(crate::ast::OnConflictAction::Update {
3616            assignments,
3617            where_,
3618        })
3619    }
3620
3621    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
3622        let mut items = Vec::new();
3623        loop {
3624            items.push(self.parse_select_item()?);
3625            if matches!(self.peek(), Token::Comma) {
3626                self.advance();
3627            } else {
3628                break;
3629            }
3630        }
3631        Ok(items)
3632    }
3633
3634    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
3635        if matches!(self.peek(), Token::Star) {
3636            self.advance();
3637            return Ok(SelectItem::Wildcard);
3638        }
3639        let expr = self.parse_expr(0)?;
3640        let alias = self.parse_optional_alias();
3641        Ok(SelectItem::Expr { expr, alias })
3642    }
3643
3644    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
3645        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
3646        // source. Detect at the head before the bare-ident fallback;
3647        // unnest is not a reserved token.
3648        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
3649            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
3650        {
3651            self.advance(); // unnest
3652            self.advance(); // (
3653            let expr = self.parse_expr(0)?;
3654            if !matches!(self.peek(), Token::RParen) {
3655                return Err(self.err(alloc::format!(
3656                    "expected ')' after unnest() argument, got {:?}",
3657                    self.peek()
3658                )));
3659            }
3660            self.advance();
3661            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns();
3662            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
3663            return Ok(TableRef {
3664                name,
3665                alias: alias_ident,
3666                as_of_segment: None,
3667                unnest_expr: Some(Box::new(expr)),
3668                unnest_column_aliases,
3669            });
3670        }
3671        let name = self.expect_ident_like()?;
3672        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
3673        // time-travel clause. Parse BEFORE the alias so the
3674        // alias can still ride at the tail (`tbl AS OF SEGMENT
3675        // '5' alias`). `AS` is a reserved keyword token, while
3676        // `OF` and `SEGMENT` are bare idents.
3677        let as_of_segment = if matches!(self.peek(), Token::As)
3678            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
3679        {
3680            self.advance(); // AS
3681            self.advance(); // OF
3682            let kw = match self.peek().clone() {
3683                Token::Ident(s) | Token::QuotedIdent(s) => s,
3684                other => {
3685                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
3686                }
3687            };
3688            if !kw.eq_ignore_ascii_case("segment") {
3689                return Err(self.err(format!(
3690                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
3691                )));
3692            }
3693            self.advance();
3694            // Segment id literal — accept either a string or
3695            // integer for operator ergonomics.
3696            let id = match self.advance() {
3697                Token::String(s) => s
3698                    .parse::<u32>()
3699                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
3700                Token::Integer(n) => u32::try_from(n)
3701                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
3702                other => {
3703                    return Err(self.err(format!(
3704                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
3705                    )));
3706                }
3707            };
3708            Some(id)
3709        } else {
3710            None
3711        };
3712        let alias = self.parse_optional_alias();
3713        Ok(TableRef {
3714            name,
3715            alias,
3716            as_of_segment,
3717            unnest_expr: None,
3718            unnest_column_aliases: Vec::new(),
3719        })
3720    }
3721
3722    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
3723    /// but also accepts `AS alias(col [, col, …])` — the
3724    /// PG-standard table-function column-list form. The column
3725    /// list is only honoured when paired with `UNNEST(...)` in
3726    /// the parent; other call sites currently discard it.
3727    fn parse_optional_alias_with_columns(&mut self) -> (Option<String>, Vec<String>) {
3728        let alias = self.parse_optional_alias();
3729        if alias.is_none() {
3730            return (None, Vec::new());
3731        }
3732        let mut cols: Vec<String> = Vec::new();
3733        if matches!(self.peek(), Token::LParen) {
3734            self.advance();
3735            loop {
3736                match self.peek().clone() {
3737                    Token::Ident(s) | Token::QuotedIdent(s) => {
3738                        self.advance();
3739                        cols.push(s);
3740                    }
3741                    _ => break,
3742                }
3743                if matches!(self.peek(), Token::Comma) {
3744                    self.advance();
3745                    continue;
3746                }
3747                break;
3748            }
3749            if matches!(self.peek(), Token::RParen) {
3750                self.advance();
3751            }
3752        }
3753        (alias, cols)
3754    }
3755
3756    /// FROM-clause: a primary table reference plus zero-or-more joined
3757    /// peers expressed via either `, <table>` (cross-product, no ON) or
3758    /// `[INNER|LEFT [OUTER]|CROSS] JOIN <table> [ON expr]`. v1.10 keeps
3759    /// the join list flat (left-associative nested-loop semantics).
3760    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
3761        let primary = self.parse_table_ref()?;
3762        let mut joins = Vec::new();
3763        loop {
3764            // `, <table>` — cross-product with no ON.
3765            if matches!(self.peek(), Token::Comma) {
3766                self.advance();
3767                let table = self.parse_table_ref()?;
3768                joins.push(FromJoin {
3769                    kind: JoinKind::Cross,
3770                    table,
3771                    on: None,
3772                });
3773                continue;
3774            }
3775            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
3776            // CROSS JOIN, and bare JOIN (defaults to INNER).
3777            let kind =
3778                match self.peek() {
3779                    Token::Inner => {
3780                        self.advance();
3781                        if !matches!(self.peek(), Token::Join) {
3782                            return Err(self
3783                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
3784                        }
3785                        self.advance();
3786                        JoinKind::Inner
3787                    }
3788                    Token::Left => {
3789                        self.advance();
3790                        if matches!(self.peek(), Token::Outer) {
3791                            self.advance();
3792                        }
3793                        if !matches!(self.peek(), Token::Join) {
3794                            return Err(self.err(format!(
3795                                "expected JOIN after LEFT [OUTER], got {:?}",
3796                                self.peek()
3797                            )));
3798                        }
3799                        self.advance();
3800                        JoinKind::Left
3801                    }
3802                    Token::Cross => {
3803                        self.advance();
3804                        if !matches!(self.peek(), Token::Join) {
3805                            return Err(self
3806                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
3807                        }
3808                        self.advance();
3809                        JoinKind::Cross
3810                    }
3811                    Token::Join => {
3812                        self.advance();
3813                        JoinKind::Inner
3814                    }
3815                    _ => break,
3816                };
3817            let table = self.parse_table_ref()?;
3818            let on = if matches!(self.peek(), Token::On) {
3819                self.advance();
3820                Some(self.parse_expr(0)?)
3821            } else if kind == JoinKind::Cross {
3822                None
3823            } else {
3824                return Err(self.err(format!(
3825                    "expected ON after {:?} JOIN, got {:?}",
3826                    kind,
3827                    self.peek()
3828                )));
3829            };
3830            joins.push(FromJoin { kind, table, on });
3831        }
3832        Ok(FromClause { primary, joins })
3833    }
3834
3835    /// Optional alias after an expression or table:
3836    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
3837    /// accepted (PG-style implicit alias). Returns `None` if the next token
3838    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
3839    fn parse_optional_alias(&mut self) -> Option<String> {
3840        if matches!(self.peek(), Token::As) {
3841            self.advance();
3842            // After AS, the next token MUST be an identifier-like — if not,
3843            // we still return None and let the caller surface the error on the
3844            // next expectation. v0.2 keeps the alias path forgiving; the
3845            // corpus tests don't exercise the malformed case.
3846            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
3847                return self.expect_ident_like().ok();
3848            }
3849            return None;
3850        }
3851        if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
3852            return self.expect_ident_like().ok();
3853        }
3854        None
3855    }
3856
3857    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
3858    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
3859        let mut lhs = self.parse_unary()?;
3860        while let Some((op, prec)) = binop_from(self.peek()) {
3861            if prec < min_prec {
3862                break;
3863            }
3864            self.advance();
3865            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
3866            // ANY is a bare ident; ALL is a reserved Token. Both
3867            // require an immediate `(` to disambiguate from
3868            // identifier columns named `any` / `all`.
3869            let any_kind = match self.peek() {
3870                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
3871                    Some(false)
3872                }
3873                Token::Ident(s) | Token::QuotedIdent(s)
3874                    if (s.eq_ignore_ascii_case("any") || s.eq_ignore_ascii_case("all"))
3875                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
3876                {
3877                    Some(s.eq_ignore_ascii_case("any"))
3878                }
3879                _ => None,
3880            };
3881            if let Some(is_any) = any_kind {
3882                self.advance(); // ident
3883                self.advance(); // (
3884                let arr = self.parse_expr(0)?;
3885                if !matches!(self.peek(), Token::RParen) {
3886                    return Err(self.err(alloc::format!(
3887                        "expected ')' after ANY/ALL argument, got {:?}",
3888                        self.peek()
3889                    )));
3890                }
3891                self.advance();
3892                lhs = Expr::AnyAll {
3893                    expr: Box::new(lhs),
3894                    op,
3895                    array: Box::new(arr),
3896                    is_any,
3897                };
3898                continue;
3899            }
3900            let rhs = self.parse_expr(prec + 1)?;
3901            lhs = Expr::Binary {
3902                lhs: Box::new(lhs),
3903                op,
3904                rhs: Box::new(rhs),
3905            };
3906        }
3907        Ok(lhs)
3908    }
3909
3910    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
3911        match self.peek() {
3912            Token::Not => {
3913                self.advance();
3914                // NOT sits between AND (2) and comparisons (4) — bind everything
3915                // ≥3, which leaves AND/OR outside.
3916                let e = self.parse_expr(3)?;
3917                Ok(Expr::Unary {
3918                    op: UnOp::Not,
3919                    expr: Box::new(e),
3920                })
3921            }
3922            Token::Minus => {
3923                self.advance();
3924                // Unary minus binds tighter than `*`/`/` (now at prec 7 after
3925                // `<->` slotted into 5 and arithmetic shifted up).
3926                let e = self.parse_expr(8)?;
3927                Ok(Expr::Unary {
3928                    op: UnOp::Neg,
3929                    expr: Box::new(e),
3930                })
3931            }
3932            _ => self.parse_atom(),
3933        }
3934    }
3935
3936    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
3937        let tok_pos = self.pos;
3938        match self.advance() {
3939            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
3940            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
3941            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
3942            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
3943            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
3944            Token::Null => Ok(Expr::Literal(Literal::Null)),
3945            // v6.1.1 — `$N` placeholder. The actual Value lookup
3946            // happens in the engine eval path against the prepared-
3947            // statement bind buffer.
3948            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
3949            Token::LParen => {
3950                // v4.10: `(SELECT ...)` in expression position is a
3951                // scalar subquery; otherwise it's a parenthesised
3952                // expression. Peek for SELECT keyword to dispatch.
3953                if matches!(self.peek(), Token::Select) {
3954                    let inner = self.parse_select_stmt()?;
3955                    match self.advance() {
3956                        Token::RParen => {
3957                            let Statement::Select(s) = inner else {
3958                                unreachable!("parse_select_stmt returns Select")
3959                            };
3960                            Ok(Expr::ScalarSubquery(Box::new(s)))
3961                        }
3962                        other => Err(ParseError {
3963                            message: format!("expected ')' after scalar subquery, got {other:?}"),
3964                            token_pos: self.pos.saturating_sub(1),
3965                        }),
3966                    }
3967                } else {
3968                    let e = self.parse_expr(0)?;
3969                    match self.advance() {
3970                        Token::RParen => Ok(e),
3971                        other => Err(ParseError {
3972                            message: format!("expected ')', got {other:?}"),
3973                            token_pos: self.pos.saturating_sub(1),
3974                        }),
3975                    }
3976                }
3977            }
3978            Token::LBracket => self.parse_vector_literal_body(),
3979            Token::Extract => self.parse_extract_atom(),
3980            Token::Interval => self.parse_interval_atom(),
3981            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
3982            // token; we match on the bare ident. NOT is a token
3983            // (consumed in the comparison rung), but `EXISTS (...)`
3984            // at the top of an expression starts here.
3985            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
3986                self.parse_exists_atom(false)
3987            }
3988            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
3989            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
3990            // CASE is a bare ident; we dispatch on lowercase match.
3991            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
3992                self.parse_case_atom()
3993            }
3994            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
3995            // is not a reserved token; we match by case-insensitive
3996            // ident. The opening `[` must follow immediately.
3997            Token::Ident(s) | Token::QuotedIdent(s)
3998                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
3999            {
4000                self.advance(); // consume `[`
4001                let mut items: Vec<Expr> = Vec::new();
4002                if !matches!(self.peek(), Token::RBracket) {
4003                    loop {
4004                        items.push(self.parse_expr(0)?);
4005                        match self.peek() {
4006                            Token::Comma => {
4007                                self.advance();
4008                            }
4009                            Token::RBracket => break,
4010                            other => {
4011                                return Err(self.err(alloc::format!(
4012                                    "expected ',' or ']' in ARRAY literal, got {other:?}"
4013                                )));
4014                            }
4015                        }
4016                    }
4017                }
4018                self.advance(); // consume `]`
4019                Ok(Expr::Array(items))
4020            }
4021            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
4022            other => Err(ParseError {
4023                message: format!("unexpected token {other:?} in expression"),
4024                token_pos: tok_pos,
4025            }),
4026        }
4027        // After parsing the atom, fold any postfix `::vector` casts.
4028        .and_then(|atom| self.finish_postfix_casts(atom))
4029    }
4030
4031    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
4032    /// Both bind tighter than any binary op.
4033    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
4034        loop {
4035            if matches!(self.peek(), Token::DoubleColon) {
4036                self.advance();
4037                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
4038                // target set to include INTERVAL (reserved Token),
4039                // TIMESTAMPTZ, and PG catalog regtype / regclass.
4040                // mailrs follow-up H3a + H3b.
4041                let target = match self.advance() {
4042                    Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
4043                        "int" | "integer" | "int4" => {
4044                            if matches!(self.peek(), Token::LBracket)
4045                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
4046                            {
4047                                self.advance();
4048                                self.advance();
4049                                CastTarget::IntArray
4050                            } else {
4051                                CastTarget::Int
4052                            }
4053                        }
4054                        "bigint" | "int8" => {
4055                            if matches!(self.peek(), Token::LBracket)
4056                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
4057                            {
4058                                self.advance();
4059                                self.advance();
4060                                CastTarget::BigIntArray
4061                            } else {
4062                                CastTarget::BigInt
4063                            }
4064                        }
4065                        "float" | "double" | "real" => CastTarget::Float,
4066                        "text" => {
4067                            // v7.10.11 — `::TEXT[]` widens to TextArray.
4068                            if matches!(self.peek(), Token::LBracket)
4069                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
4070                            {
4071                                self.advance();
4072                                self.advance();
4073                                CastTarget::TextArray
4074                            } else {
4075                                CastTarget::Text
4076                            }
4077                        }
4078                        "bool" | "boolean" => CastTarget::Bool,
4079                        "vector" => CastTarget::Vector,
4080                        "date" => CastTarget::Date,
4081                        "timestamp" | "datetime" => CastTarget::Timestamp,
4082                        "timestamptz" => CastTarget::Timestamptz,
4083                        "interval" => CastTarget::Interval,
4084                        "json" => CastTarget::Json,
4085                        "jsonb" => CastTarget::Jsonb,
4086                        "regtype" => CastTarget::RegType,
4087                        "regclass" => CastTarget::RegClass,
4088                        // v7.12.0 — `::tsvector` / `::tsquery`.
4089                        // Engine decodes the LHS text via the PG
4090                        // external form parser.
4091                        "tsvector" => CastTarget::TsVector,
4092                        "tsquery" => CastTarget::TsQuery,
4093                        other => {
4094                            return Err(ParseError {
4095                                message: format!("unsupported cast target `::{other}`"),
4096                                token_pos: self.pos.saturating_sub(1),
4097                            });
4098                        }
4099                    },
4100                    Token::Interval => CastTarget::Interval,
4101                    other => {
4102                        return Err(ParseError {
4103                            message: format!("expected type ident after `::`, got {other:?}"),
4104                            token_pos: self.pos.saturating_sub(1),
4105                        });
4106                    }
4107                };
4108                expr = Expr::Cast {
4109                    expr: Box::new(expr),
4110                    target,
4111                };
4112                continue;
4113            }
4114            if matches!(self.peek(), Token::Is) {
4115                self.advance();
4116                let negated = if matches!(self.peek(), Token::Not) {
4117                    self.advance();
4118                    true
4119                } else {
4120                    false
4121                };
4122                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
4123                // mailrs pg_dump.
4124                if matches!(self.peek(), Token::Distinct) {
4125                    self.advance();
4126                    if !matches!(self.peek(), Token::From) {
4127                        return Err(self.err(format!(
4128                            "expected FROM after IS{} DISTINCT, got {:?}",
4129                            if negated { " NOT" } else { "" },
4130                            self.peek()
4131                        )));
4132                    }
4133                    self.advance();
4134                    // Right-hand side: parse at the same precedence
4135                    // tier as comparison so `x IS DISTINCT FROM a + b`
4136                    // groups as `x IS DISTINCT FROM (a + b)`.
4137                    let rhs = self.parse_expr(20)?;
4138                    let op = if negated {
4139                        BinOp::IsNotDistinctFrom
4140                    } else {
4141                        BinOp::IsDistinctFrom
4142                    };
4143                    expr = Expr::Binary {
4144                        op,
4145                        lhs: Box::new(expr),
4146                        rhs: Box::new(rhs),
4147                    };
4148                    continue;
4149                }
4150                if !matches!(self.peek(), Token::Null) {
4151                    return Err(self.err(format!(
4152                        "expected NULL or DISTINCT after IS{}, got {:?}",
4153                        if negated { " NOT" } else { "" },
4154                        self.peek()
4155                    )));
4156                }
4157                self.advance();
4158                expr = Expr::IsNull {
4159                    expr: Box::new(expr),
4160                    negated,
4161                };
4162                continue;
4163            }
4164            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
4165            // Look one token ahead so a stray `NOT` not followed by any of
4166            // these flows through to the early return below untouched.
4167            let negated = if matches!(self.peek(), Token::Not) {
4168                let next = self.tokens.get(self.pos + 1);
4169                matches!(next, Some(Token::Between | Token::In | Token::Like))
4170            } else {
4171                false
4172            };
4173            if negated {
4174                self.advance();
4175            }
4176            if matches!(self.peek(), Token::Between) {
4177                expr = self.parse_between_tail(expr, negated)?;
4178                continue;
4179            }
4180            if matches!(self.peek(), Token::In) {
4181                expr = self.parse_in_tail(expr, negated)?;
4182                continue;
4183            }
4184            if matches!(self.peek(), Token::Like) {
4185                self.advance();
4186                // Pattern at the same precedence as other comparison RHSes —
4187                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
4188                let pattern = self.parse_expr(5)?;
4189                expr = Expr::Like {
4190                    expr: Box::new(expr),
4191                    pattern: Box::new(pattern),
4192                    negated,
4193                };
4194                continue;
4195            }
4196            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
4197            // returns NULL for out-of-range. Multiple subscripts
4198            // chain: `a[i][j]` parses left-to-right.
4199            if matches!(self.peek(), Token::LBracket) {
4200                self.advance();
4201                let index = self.parse_expr(0)?;
4202                if !matches!(self.peek(), Token::RBracket) {
4203                    return Err(self.err(alloc::format!(
4204                        "expected ']' after array index, got {:?}",
4205                        self.peek()
4206                    )));
4207                }
4208                self.advance();
4209                expr = Expr::ArraySubscript {
4210                    target: Box::new(expr),
4211                    index: Box::new(index),
4212                };
4213                continue;
4214            }
4215            return Ok(expr);
4216        }
4217    }
4218
4219    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
4220    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
4221    /// `AND` is not swallowed.
4222    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
4223        self.advance(); // BETWEEN
4224        let low = self.parse_expr(5)?;
4225        if !matches!(self.peek(), Token::And) {
4226            return Err(self.err(format!(
4227                "expected AND after BETWEEN low bound, got {:?}",
4228                self.peek()
4229            )));
4230        }
4231        self.advance();
4232        let high = self.parse_expr(5)?;
4233        let target = Box::new(expr);
4234        let combined = Expr::Binary {
4235            lhs: Box::new(Expr::Binary {
4236                lhs: target.clone(),
4237                op: BinOp::GtEq,
4238                rhs: Box::new(low),
4239            }),
4240            op: BinOp::And,
4241            rhs: Box::new(Expr::Binary {
4242                lhs: target,
4243                op: BinOp::LtEq,
4244                rhs: Box::new(high),
4245            }),
4246        };
4247        Ok(maybe_not(combined, negated))
4248    }
4249
4250    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
4251    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
4252    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
4253    /// Caller already consumed the leading `WITH` ident.
4254    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
4255        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
4256        // Comes through as an identifier; consume it if present and
4257        // mark every CTE in the clause as recursive (PG semantics —
4258        // the flag is per-WITH, not per-CTE).
4259        let mut recursive = false;
4260        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4261            && s.eq_ignore_ascii_case("recursive")
4262        {
4263            self.advance();
4264            recursive = true;
4265        }
4266        let mut ctes = Vec::new();
4267        loop {
4268            let name = self.expect_ident_like()?;
4269            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
4270            // PG uses these to rename the body's output columns; we
4271            // do the same below by overriding `columns[i].name`.
4272            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
4273                self.advance();
4274                let mut names = Vec::new();
4275                loop {
4276                    names.push(self.expect_ident_like()?);
4277                    if matches!(self.peek(), Token::Comma) {
4278                        self.advance();
4279                        continue;
4280                    }
4281                    break;
4282                }
4283                if !matches!(self.peek(), Token::RParen) {
4284                    return Err(self.err(format!(
4285                        "expected ')' to close CTE column list, got {:?}",
4286                        self.peek()
4287                    )));
4288                }
4289                self.advance();
4290                names
4291            } else {
4292                Vec::new()
4293            };
4294            // AS is a reserved Token::As (used by SELECT-item / FROM
4295            // aliasing) — handle it specially rather than as a bare
4296            // ident.
4297            if !matches!(self.peek(), Token::As) {
4298                return Err(self.err(format!(
4299                    "expected AS after CTE name {name:?}, got {:?}",
4300                    self.peek()
4301                )));
4302            }
4303            self.advance();
4304            if !matches!(self.peek(), Token::LParen) {
4305                return Err(self.err(format!(
4306                    "expected '(' after AS in WITH clause, got {:?}",
4307                    self.peek()
4308                )));
4309            }
4310            self.advance();
4311            if !matches!(self.peek(), Token::Select) {
4312                return Err(self.err(format!("WITH body must be a SELECT, got {:?}", self.peek())));
4313            }
4314            let inner = self.parse_select_stmt()?;
4315            if !matches!(self.peek(), Token::RParen) {
4316                return Err(self.err(format!(
4317                    "expected ')' after CTE body, got {:?}",
4318                    self.peek()
4319                )));
4320            }
4321            self.advance();
4322            let Statement::Select(body) = inner else {
4323                unreachable!("parse_select_stmt returns Select")
4324            };
4325            ctes.push(crate::ast::Cte {
4326                name,
4327                body,
4328                recursive,
4329                column_overrides,
4330            });
4331            if matches!(self.peek(), Token::Comma) {
4332                self.advance();
4333                continue;
4334            }
4335            break;
4336        }
4337        // The body SELECT follows. Must start with SELECT.
4338        if !matches!(self.peek(), Token::Select) {
4339            return Err(self.err(format!(
4340                "expected SELECT after WITH clause, got {:?}",
4341                self.peek()
4342            )));
4343        }
4344        let body_stmt = self.parse_select_stmt()?;
4345        let Statement::Select(mut body) = body_stmt else {
4346            unreachable!()
4347        };
4348        body.ctes = ctes;
4349        Ok(Statement::Select(body))
4350    }
4351
4352    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
4353    /// already consumed the leading `EXISTS` ident via
4354    /// `self.advance()`.
4355    /// v7.13.0 — parse the rest of a `CASE … END` expression after
4356    /// the leading `CASE` ident has been consumed (mailrs round-5
4357    /// G9). Supports both the searched form
4358    /// (`CASE WHEN cond THEN val …`) and the simple form
4359    /// (`CASE operand WHEN val THEN val …`).
4360    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
4361        // Disambiguate searched vs simple form: if the next token
4362        // is `WHEN`, we're in the searched form. Otherwise the
4363        // intervening expression is the operand.
4364        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
4365            None
4366        } else {
4367            Some(Box::new(self.parse_expr(0)?))
4368        };
4369        let mut branches: Vec<(Expr, Expr)> = Vec::new();
4370        loop {
4371            match self.peek() {
4372                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
4373                    self.advance();
4374                    let cond = self.parse_expr(0)?;
4375                    match self.peek() {
4376                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
4377                            self.advance();
4378                        }
4379                        other => {
4380                            return Err(self.err(alloc::format!(
4381                                "expected THEN after CASE WHEN <expr>, got {other:?}"
4382                            )));
4383                        }
4384                    }
4385                    let value = self.parse_expr(0)?;
4386                    branches.push((cond, value));
4387                }
4388                _ => break,
4389            }
4390        }
4391        if branches.is_empty() {
4392            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
4393        }
4394        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
4395        {
4396            self.advance();
4397            Some(Box::new(self.parse_expr(0)?))
4398        } else {
4399            None
4400        };
4401        match self.peek() {
4402            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
4403                self.advance();
4404            }
4405            other => {
4406                return Err(self.err(alloc::format!(
4407                    "expected END to close CASE expression, got {other:?}"
4408                )));
4409            }
4410        }
4411        Ok(Expr::Case {
4412            operand,
4413            branches,
4414            else_branch,
4415        })
4416    }
4417
4418    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
4419        if !matches!(self.peek(), Token::LParen) {
4420            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
4421        }
4422        self.advance();
4423        let inner = self.parse_select_stmt()?;
4424        if !matches!(self.peek(), Token::RParen) {
4425            return Err(self.err(format!(
4426                "expected ')' after EXISTS-subquery, got {:?}",
4427                self.peek()
4428            )));
4429        }
4430        self.advance();
4431        let Statement::Select(s) = inner else {
4432            unreachable!("parse_select_stmt returns Select")
4433        };
4434        Ok(Expr::Exists {
4435            subquery: Box::new(s),
4436            negated,
4437        })
4438    }
4439
4440    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
4441        self.advance(); // IN
4442        if !matches!(self.peek(), Token::LParen) {
4443            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
4444        }
4445        self.advance();
4446        // v4.10: `IN (SELECT ...)` — subquery branch.
4447        if matches!(self.peek(), Token::Select) {
4448            let inner = self.parse_select_stmt()?;
4449            if !matches!(self.peek(), Token::RParen) {
4450                return Err(self.err(format!(
4451                    "expected ')' after IN-subquery, got {:?}",
4452                    self.peek()
4453                )));
4454            }
4455            self.advance();
4456            let Statement::Select(s) = inner else {
4457                unreachable!("parse_select_stmt always returns Statement::Select")
4458            };
4459            return Ok(Expr::InSubquery {
4460                expr: Box::new(expr),
4461                subquery: Box::new(s),
4462                negated,
4463            });
4464        }
4465        let mut elements = Vec::new();
4466        if !matches!(self.peek(), Token::RParen) {
4467            loop {
4468                elements.push(self.parse_expr(0)?);
4469                match self.peek() {
4470                    Token::Comma => {
4471                        self.advance();
4472                    }
4473                    Token::RParen => break,
4474                    other => {
4475                        return Err(
4476                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
4477                        );
4478                    }
4479                }
4480            }
4481        }
4482        self.advance(); // ')'
4483        let target = Box::new(expr);
4484        let combined = if elements.is_empty() {
4485            Expr::Literal(Literal::Bool(false))
4486        } else {
4487            let mut iter = elements.into_iter();
4488            let first = iter.next().unwrap();
4489            let mut acc = Expr::Binary {
4490                lhs: target.clone(),
4491                op: BinOp::Eq,
4492                rhs: Box::new(first),
4493            };
4494            for elt in iter {
4495                acc = Expr::Binary {
4496                    lhs: Box::new(acc),
4497                    op: BinOp::Or,
4498                    rhs: Box::new(Expr::Binary {
4499                        lhs: target.clone(),
4500                        op: BinOp::Eq,
4501                        rhs: Box::new(elt),
4502                    }),
4503                };
4504            }
4505            acc
4506        };
4507        Ok(maybe_not(combined, negated))
4508    }
4509
4510    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
4511    /// already consumed by the caller. Elements must be numeric literals
4512    /// (with optional unary `-`); any compound expression is rejected at
4513    /// parse time so the runtime never needs to evaluate inside a vector.
4514    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
4515    /// has already consumed the `EXTRACT` token before calling us —
4516    /// we pick up at the opening `(`.
4517    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
4518        if !matches!(self.peek(), Token::LParen) {
4519            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
4520        }
4521        self.advance();
4522        let field_name = self.expect_ident_like()?;
4523        let field = match field_name.to_ascii_lowercase().as_str() {
4524            "year" => ExtractField::Year,
4525            "month" => ExtractField::Month,
4526            "day" => ExtractField::Day,
4527            "hour" => ExtractField::Hour,
4528            "minute" => ExtractField::Minute,
4529            "second" => ExtractField::Second,
4530            "microsecond" | "microseconds" => ExtractField::Microsecond,
4531            other => {
4532                return Err(self.err(format!(
4533                    "unknown EXTRACT field {other:?}; \
4534                     supported: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MICROSECOND"
4535                )));
4536            }
4537        };
4538        if !matches!(self.peek(), Token::From) {
4539            return Err(self.err(format!(
4540                "expected FROM after EXTRACT field, got {:?}",
4541                self.peek()
4542            )));
4543        }
4544        self.advance();
4545        let source = self.parse_expr(0)?;
4546        if !matches!(self.peek(), Token::RParen) {
4547            return Err(self.err(format!(
4548                "expected ')' to close EXTRACT, got {:?}",
4549                self.peek()
4550            )));
4551        }
4552        self.advance();
4553        Ok(Expr::Extract {
4554            field,
4555            source: Box::new(source),
4556        })
4557    }
4558
4559    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
4560    /// is already consumed; we expect a single string literal next and
4561    /// resolve it into `Literal::Interval` at parse time so the engine
4562    /// never has to re-tokenise inside the string.
4563    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
4564        let tok = self.advance();
4565        let Token::String(text) = tok else {
4566            return Err(self.err(format!(
4567                "expected string literal after INTERVAL, got {tok:?}"
4568            )));
4569        };
4570        let (months, micros) = parse_interval_text(&text).ok_or_else(|| ParseError {
4571            message: format!(
4572                "cannot parse INTERVAL {text:?}; \
4573                     expected `<n> <unit> [<n> <unit> ...]` with units \
4574                     microsecond[s], millisecond[s], second[s], minute[s], \
4575                     hour[s], day[s], week[s], month[s], year[s]"
4576            ),
4577            token_pos: self.pos.saturating_sub(1),
4578        })?;
4579        Ok(Expr::Literal(Literal::Interval {
4580            months,
4581            micros,
4582            text,
4583        }))
4584    }
4585
4586    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
4587        let mut elems = Vec::new();
4588        if matches!(self.peek(), Token::RBracket) {
4589            self.advance();
4590            return Ok(Expr::Literal(Literal::Vector(elems)));
4591        }
4592        loop {
4593            let e = self.parse_expr(0)?;
4594            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
4595                message: format!("vector element must be a numeric literal, got {e:?}"),
4596                token_pos: self.pos,
4597            })?;
4598            elems.push(x);
4599            match self.peek() {
4600                Token::Comma => {
4601                    self.advance();
4602                }
4603                Token::RBracket => {
4604                    self.advance();
4605                    break;
4606                }
4607                other => {
4608                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
4609                }
4610            }
4611        }
4612        Ok(Expr::Literal(Literal::Vector(elems)))
4613    }
4614
4615    /// Atom that started with an identifier: could be `t.col`, `col`, or
4616    /// `func(arg, ...)`. Detect each shape by looking at the next token.
4617    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
4618    /// [, ...])`. Caller has already consumed `OVER`. Either clause
4619    /// is optional; an empty `()` is also legal (PG semantics).
4620    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
4621    /// modifier between `name(args)` and `OVER (...)`. Default is
4622    /// `Respect`. Unrecognised idents leave the stream unchanged.
4623    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
4624        let Token::Ident(s) = self.peek().clone() else {
4625            return NullTreatment::Respect;
4626        };
4627        let is_ignore = s.eq_ignore_ascii_case("ignore");
4628        let is_respect = s.eq_ignore_ascii_case("respect");
4629        if !is_ignore && !is_respect {
4630            return NullTreatment::Respect;
4631        }
4632        // Lookahead for NULLS — only consume both tokens together.
4633        // pos+1 must hold a "nulls" ident.
4634        if self.pos + 1 < self.tokens.len()
4635            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
4636            && s2.eq_ignore_ascii_case("nulls")
4637        {
4638            self.advance();
4639            self.advance();
4640            return if is_ignore {
4641                NullTreatment::Ignore
4642            } else {
4643                NullTreatment::Respect
4644            };
4645        }
4646        NullTreatment::Respect
4647    }
4648
4649    /// No frame clause is supported.
4650    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
4651    fn parse_over_clause(
4652        &mut self,
4653    ) -> Result<(Vec<Expr>, Vec<(Expr, bool)>, Option<WindowFrame>), ParseError> {
4654        if !matches!(self.peek(), Token::LParen) {
4655            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
4656        }
4657        self.advance();
4658        let mut partition_by = Vec::new();
4659        let mut order_by = Vec::new();
4660        // PARTITION BY ?
4661        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4662            && s.eq_ignore_ascii_case("partition")
4663        {
4664            self.advance();
4665            if !matches!(self.peek(), Token::By) {
4666                return Err(self.err(format!(
4667                    "expected BY after PARTITION, got {:?}",
4668                    self.peek()
4669                )));
4670            }
4671            self.advance();
4672            loop {
4673                partition_by.push(self.parse_expr(0)?);
4674                if matches!(self.peek(), Token::Comma) {
4675                    self.advance();
4676                    continue;
4677                }
4678                break;
4679            }
4680        }
4681        // ORDER BY ?
4682        if matches!(self.peek(), Token::Order) {
4683            self.advance();
4684            if !matches!(self.peek(), Token::By) {
4685                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
4686            }
4687            self.advance();
4688            loop {
4689                let e = self.parse_expr(0)?;
4690                let desc = if matches!(self.peek(), Token::Desc) {
4691                    self.advance();
4692                    true
4693                } else if matches!(self.peek(), Token::Asc) {
4694                    self.advance();
4695                    false
4696                } else {
4697                    false
4698                };
4699                order_by.push((e, desc));
4700                if matches!(self.peek(), Token::Comma) {
4701                    self.advance();
4702                    continue;
4703                }
4704                break;
4705            }
4706        }
4707        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
4708        // Both keywords come through the lexer as identifiers; match
4709        // case-insensitively.
4710        let mut frame: Option<WindowFrame> = None;
4711        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4712            let kind = if s.eq_ignore_ascii_case("rows") {
4713                Some(FrameKind::Rows)
4714            } else if s.eq_ignore_ascii_case("range") {
4715                Some(FrameKind::Range)
4716            } else {
4717                None
4718            };
4719            if let Some(kind) = kind {
4720                self.advance();
4721                frame = Some(self.parse_frame_tail(kind)?);
4722            }
4723        }
4724        if !matches!(self.peek(), Token::RParen) {
4725            return Err(self.err(format!(
4726                "expected ')' to close OVER clause, got {:?}",
4727                self.peek()
4728            )));
4729        }
4730        self.advance();
4731        Ok((partition_by, order_by, frame))
4732    }
4733
4734    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
4735    /// or `RANGE` keyword was just consumed. Accepts both
4736    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
4737    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
4738    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
4739    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
4740        if matches!(self.peek(), Token::Between) {
4741            self.advance();
4742            let start = self.parse_frame_bound()?;
4743            if !matches!(self.peek(), Token::And) {
4744                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
4745            }
4746            self.advance();
4747            let end = self.parse_frame_bound()?;
4748            Ok(WindowFrame {
4749                kind,
4750                start,
4751                end: Some(end),
4752            })
4753        } else {
4754            let start = self.parse_frame_bound()?;
4755            Ok(WindowFrame {
4756                kind,
4757                start,
4758                end: None,
4759            })
4760        }
4761    }
4762
4763    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
4764    /// `CURRENT ROW`, `<n> FOLLOWING`, `UNBOUNDED FOLLOWING`.
4765    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
4766        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
4767        if let Token::Integer(n) = *self.peek() {
4768            self.advance();
4769            let n: u64 = u64::try_from(n).map_err(|_| {
4770                self.err(format!(
4771                    "invalid frame offset {n} — expected non-negative integer"
4772                ))
4773            })?;
4774            let dir = self.expect_ident_like()?;
4775            return if dir.eq_ignore_ascii_case("preceding") {
4776                Ok(FrameBound::OffsetPreceding(n))
4777            } else if dir.eq_ignore_ascii_case("following") {
4778                Ok(FrameBound::OffsetFollowing(n))
4779            } else {
4780                Err(self.err(format!(
4781                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
4782                )))
4783            };
4784        }
4785        let first = self.expect_ident_like()?;
4786        if first.eq_ignore_ascii_case("unbounded") {
4787            let dir = self.expect_ident_like()?;
4788            return if dir.eq_ignore_ascii_case("preceding") {
4789                Ok(FrameBound::UnboundedPreceding)
4790            } else if dir.eq_ignore_ascii_case("following") {
4791                Ok(FrameBound::UnboundedFollowing)
4792            } else {
4793                Err(self.err(format!(
4794                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
4795                )))
4796            };
4797        }
4798        if first.eq_ignore_ascii_case("current") {
4799            let row = self.expect_ident_like()?;
4800            if !row.eq_ignore_ascii_case("row") {
4801                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
4802            }
4803            return Ok(FrameBound::CurrentRow);
4804        }
4805        Err(self.err(format!(
4806            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
4807        )))
4808    }
4809
4810    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
4811        if matches!(self.peek(), Token::Dot) {
4812            self.advance();
4813            let name = self.expect_ident_like()?;
4814            return Ok(Expr::Column(ColumnName {
4815                qualifier: Some(first),
4816                name,
4817            }));
4818        }
4819        if matches!(self.peek(), Token::LParen) {
4820            self.advance();
4821            // `COUNT(*)` — special-cased here because `*` isn't a normal
4822            // expression token. Lower-case match on `first` since the lexer
4823            // folds identifiers.
4824            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
4825                self.advance();
4826                if !matches!(self.peek(), Token::RParen) {
4827                    return Err(self.err(format!(
4828                        "expected ')' after COUNT(*), got {:?}",
4829                        self.peek()
4830                    )));
4831                }
4832                self.advance();
4833                // v4.12: COUNT(*) OVER (...) — same window tail.
4834                let null_treatment = self.parse_null_treatment_modifier();
4835                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4836                    && s.eq_ignore_ascii_case("over")
4837                {
4838                    self.advance();
4839                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
4840                    return Ok(Expr::WindowFunction {
4841                        name: "count_star".into(),
4842                        args: Vec::new(),
4843                        partition_by,
4844                        order_by,
4845                        frame,
4846                        null_treatment,
4847                    });
4848                }
4849                return Ok(Expr::FunctionCall {
4850                    name: "count_star".into(),
4851                    args: Vec::new(),
4852                });
4853            }
4854            // Function call. PG-style: zero-or-more comma-separated args.
4855            let mut args = Vec::new();
4856            if !matches!(self.peek(), Token::RParen) {
4857                loop {
4858                    args.push(self.parse_expr(0)?);
4859                    match self.peek() {
4860                        Token::Comma => {
4861                            self.advance();
4862                        }
4863                        Token::RParen => break,
4864                        other => {
4865                            return Err(self.err(format!(
4866                                "expected ',' or ')' in function args, got {other:?}"
4867                            )));
4868                        }
4869                    }
4870                }
4871            }
4872            self.advance(); // consume ')'
4873            // v4.12: window-function tail — `name(args) OVER (...)`.
4874            // Promotes the just-parsed FunctionCall into a
4875            // WindowFunction node carrying partition + order.
4876            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
4877            // / `RESPECT NULLS OVER (...)` between the closing paren
4878            // and `OVER`.
4879            let null_treatment = self.parse_null_treatment_modifier();
4880            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4881                && s.eq_ignore_ascii_case("over")
4882            {
4883                self.advance();
4884                let (partition_by, order_by, frame) = self.parse_over_clause()?;
4885                return Ok(Expr::WindowFunction {
4886                    name: first,
4887                    args,
4888                    partition_by,
4889                    order_by,
4890                    frame,
4891                    null_treatment,
4892                });
4893            }
4894            return Ok(Expr::FunctionCall { name: first, args });
4895        }
4896        // v7.9.20 — SQL-standard parenless keyword expressions
4897        // (PG treats these as functions called without parens).
4898        // Resolve to a synthetic FunctionCall so the engine's
4899        // eval path reuses the existing function-call routing.
4900        // mailrs G3.
4901        let lc = first.to_ascii_lowercase();
4902        if matches!(
4903            lc.as_str(),
4904            "current_date" | "current_time" | "current_timestamp" | "localtimestamp" | "localtime"
4905        ) {
4906            return Ok(Expr::FunctionCall {
4907                name: lc,
4908                args: Vec::new(),
4909            });
4910        }
4911        Ok(Expr::Column(ColumnName {
4912            qualifier: None,
4913            name: first,
4914        }))
4915    }
4916}
4917
4918/// v6.8.2 — walk an expression tree and return the first column
4919/// reference's bare name. Used by `parse_create_index_stmt_after_create`
4920/// to derive `CreateIndexStatement.column` from an expression
4921/// key (so downstream planner code resolving a primary column
4922/// position keeps working with expression indexes). Returns
4923/// `None` when the expression has no column ref at all — caller
4924/// surfaces that as a parse error.
4925fn extract_first_column(expr: &Expr) -> Option<String> {
4926    match expr {
4927        Expr::Column(cn) => Some(cn.name.clone()),
4928        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
4929        Expr::Binary { lhs, rhs, .. } => {
4930            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
4931        }
4932        Expr::Unary { expr: e, .. } => extract_first_column(e),
4933        _ => None,
4934    }
4935}
4936
4937fn maybe_not(expr: Expr, negated: bool) -> Expr {
4938    if negated {
4939        Expr::Unary {
4940            op: UnOp::Not,
4941            expr: Box::new(expr),
4942        }
4943    } else {
4944        expr
4945    }
4946}
4947
4948fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
4949    let pair = match tok {
4950        Token::Or => (BinOp::Or, 1),
4951        Token::And => (BinOp::And, 2),
4952        Token::Eq => (BinOp::Eq, 4),
4953        Token::NotEq => (BinOp::NotEq, 4),
4954        Token::Lt => (BinOp::Lt, 4),
4955        Token::LtEq => (BinOp::LtEq, 4),
4956        Token::Gt => (BinOp::Gt, 4),
4957        Token::GtEq => (BinOp::GtEq, 4),
4958        // pgvector distance ops all sit on the same rung — tighter than
4959        // comparisons (4) so `col <-> v < threshold` parses correctly.
4960        Token::L2Distance => (BinOp::L2Distance, 5),
4961        Token::InnerProduct => (BinOp::InnerProduct, 5),
4962        Token::CosineDistance => (BinOp::CosineDistance, 5),
4963        Token::Plus => (BinOp::Add, 6),
4964        Token::Minus => (BinOp::Sub, 6),
4965        // `||` sits beside `+`/`-` (matches PG conceptually — concat groups
4966        // by the same level as binary additive arithmetic).
4967        Token::Concat => (BinOp::Concat, 6),
4968        Token::Star => (BinOp::Mul, 7),
4969        Token::Slash => (BinOp::Div, 7),
4970        // v4.14: JSON path ops bind tighter than comparisons (4)
4971        // and additive (6) so `doc->'k' = 'v'` parses correctly.
4972        // Same rung as the multiplicative ops.
4973        Token::JsonGet => (BinOp::JsonGet, 7),
4974        Token::JsonGetText => (BinOp::JsonGetText, 7),
4975        Token::JsonGetPath => (BinOp::JsonGetPath, 7),
4976        Token::JsonGetPathText => (BinOp::JsonGetPathText, 7),
4977        Token::JsonContains => (BinOp::JsonContains, 7),
4978        // v7.12.2 — `@@` binds at the comparison rung (looser than
4979        // arithmetic, tighter than AND / OR). PG places `@@` at
4980        // the same precedence as `=` / `<`, so we follow.
4981        Token::TsMatch => (BinOp::TsMatch, 4),
4982        _ => return None,
4983    };
4984    Some(pair)
4985}
4986
4987#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4988// `as f32` here is intentional: vector elements widen / narrow into f32 on
4989// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
4990// past ~15 decimal digits — both are acceptable for a fixed-precision
4991// pgvector column.
4992fn extract_numeric_literal(e: &Expr) -> Option<f32> {
4993    match e {
4994        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
4995        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
4996        Expr::Unary {
4997            op: UnOp::Neg,
4998            expr,
4999        } => extract_numeric_literal(expr).map(|x| -x),
5000        _ => None,
5001    }
5002}
5003
5004/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
5005/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
5006/// negative. Returns `None` if any pair fails to parse or no pair is found.
5007///
5008/// Recognised units (case-insensitive, optional trailing `s`):
5009/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
5010/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
5011pub fn parse_interval_text(s: &str) -> Option<(i32, i64)> {
5012    let parts: Vec<&str> = s.split_whitespace().collect();
5013    if parts.is_empty() || !parts.len().is_multiple_of(2) {
5014        return None;
5015    }
5016    let mut months: i32 = 0;
5017    let mut micros: i64 = 0;
5018    let mut i = 0;
5019    while i < parts.len() {
5020        let n: i64 = parts[i].parse().ok()?;
5021        let unit = parts[i + 1].to_ascii_lowercase();
5022        let unit_stripped = unit.strip_suffix('s').unwrap_or(&unit);
5023        match unit_stripped {
5024            "microsecond" => micros = micros.checked_add(n)?,
5025            "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
5026            "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
5027            "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
5028            "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
5029            "day" => micros = micros.checked_add(n.checked_mul(86_400_000_000)?)?,
5030            "week" => micros = micros.checked_add(n.checked_mul(604_800_000_000)?)?,
5031            "month" => {
5032                let n32 = i32::try_from(n).ok()?;
5033                months = months.checked_add(n32)?;
5034            }
5035            "year" => {
5036                let n32 = i32::try_from(n).ok()?;
5037                months = months.checked_add(n32.checked_mul(12)?)?;
5038            }
5039            _ => return None,
5040        }
5041        i += 2;
5042    }
5043    Some((months, micros))
5044}
5045
5046/// v7.12.4 — map a bare type-name identifier (the form that
5047/// appears in a function arg list or RETURNS clause) to a
5048/// [`ColumnTypeName`]. Returns `None` for unknown / extension
5049/// types so the caller can preserve them as
5050/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
5051///
5052/// Subset of the full column-type grammar — we deliberately
5053/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
5054/// here because function-arg types in v7.12.4 are mostly the
5055/// bare form (`text`, `int`, `bytea`, …).
5056fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
5057    Some(match ident.to_ascii_lowercase().as_str() {
5058        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
5059        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
5060        "bigint" => ColumnTypeName::BigInt,
5061        "float" | "double" | "real" => ColumnTypeName::Float,
5062        "text" => ColumnTypeName::Text,
5063        "bool" | "boolean" => ColumnTypeName::Bool,
5064        "date" => ColumnTypeName::Date,
5065        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
5066        "timestamptz" => ColumnTypeName::Timestamptz,
5067        "json" => ColumnTypeName::Json,
5068        "jsonb" => ColumnTypeName::Jsonb,
5069        "bytea" | "bytes" => ColumnTypeName::Bytes,
5070        "tsvector" => ColumnTypeName::TsVector,
5071        "tsquery" => ColumnTypeName::TsQuery,
5072        _ => return None,
5073    })
5074}
5075
5076/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
5077/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
5078///
5079/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
5080/// / embedded SQL land in v7.12.5+):
5081///
5082/// ```text
5083///   body          := [ws] block [ws]
5084///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
5085///   stmt          := assign | return
5086///   assign        := assign_target := expr
5087///   assign_target := ( NEW | OLD ) . ident | ident
5088///   return        := RETURN ( NEW | OLD | NULL | expr )
5089/// ```
5090///
5091/// `expr` is parsed by recursing into the regular `Parser` — so a
5092/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
5093/// NEW.subject || ' ' || NEW.sender)` body shape works without
5094/// the body parser knowing what `to_tsvector` is.
5095///
5096/// Errors here cause the caller to fall back to
5097/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
5098/// successful, but the executor will refuse to invoke the
5099/// function with an "unparseable body" error.
5100/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
5101/// from the crate root as `spg_sql::parse_function_body`.
5102pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
5103    parse_plpgsql_body(body)
5104}
5105
5106fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
5107    // Use the regular lexer on the body text. The trailing
5108    // `END;` may or may not have a semicolon; the lexer treats
5109    // both forms identically.
5110    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
5111        message: alloc::format!("plpgsql body lex error: {e}"),
5112        token_pos: 0,
5113    })?;
5114    let mut parser = Parser::new(tokens);
5115    parser.parse_plpgsql_block()
5116}
5117
5118#[cfg(test)]
5119mod tests {
5120    use super::*;
5121    use alloc::string::ToString;
5122
5123    fn parse(s: &str) -> Statement {
5124        parse_statement(s).expect("parse ok")
5125    }
5126
5127    fn lit_int(n: i64) -> Expr {
5128        Expr::Literal(Literal::Integer(n))
5129    }
5130
5131    fn col(name: &str) -> Expr {
5132        Expr::Column(ColumnName {
5133            qualifier: None,
5134            name: name.into(),
5135        })
5136    }
5137
5138    #[test]
5139    fn select_single_integer() {
5140        let s = parse("SELECT 1");
5141        let Statement::Select(s) = s else {
5142            panic!("expected SELECT")
5143        };
5144        assert_eq!(s.items.len(), 1);
5145        assert!(s.from.is_none());
5146        assert!(s.where_.is_none());
5147    }
5148
5149    #[test]
5150    fn select_multiple_literal_kinds() {
5151        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
5152        let Statement::Select(s) = s else {
5153            panic!("expected SELECT")
5154        };
5155        assert_eq!(s.items.len(), 5);
5156    }
5157
5158    #[test]
5159    fn select_wildcard_from_table() {
5160        let s = parse("SELECT * FROM users");
5161        let Statement::Select(s) = s else {
5162            panic!("expected SELECT")
5163        };
5164        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
5165        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
5166    }
5167
5168    #[test]
5169    fn select_with_table_alias() {
5170        let s = parse("SELECT * FROM users AS u");
5171        let Statement::Select(s) = s else {
5172            panic!("expected SELECT")
5173        };
5174        let t = &s.from.as_ref().unwrap().primary;
5175        assert_eq!(t.name, "users");
5176        assert_eq!(t.alias.as_deref(), Some("u"));
5177    }
5178
5179    #[test]
5180    fn select_with_where_eq() {
5181        let s = parse("SELECT a FROM t WHERE a = 1");
5182        let Statement::Select(s) = s else {
5183            panic!("expected SELECT")
5184        };
5185        let w = s.where_.unwrap();
5186        assert_eq!(
5187            w,
5188            Expr::Binary {
5189                lhs: Box::new(col("a")),
5190                op: BinOp::Eq,
5191                rhs: Box::new(lit_int(1)),
5192            }
5193        );
5194    }
5195
5196    #[test]
5197    fn arithmetic_precedence() {
5198        let s = parse("SELECT 1 + 2 * 3");
5199        let Statement::Select(s) = s else {
5200            panic!("expected SELECT")
5201        };
5202        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5203            panic!("wildcard?")
5204        };
5205        assert_eq!(
5206            expr,
5207            &Expr::Binary {
5208                lhs: Box::new(lit_int(1)),
5209                op: BinOp::Add,
5210                rhs: Box::new(Expr::Binary {
5211                    lhs: Box::new(lit_int(2)),
5212                    op: BinOp::Mul,
5213                    rhs: Box::new(lit_int(3)),
5214                }),
5215            }
5216        );
5217    }
5218
5219    #[test]
5220    fn parentheses_override_precedence() {
5221        let s = parse("SELECT (1 + 2) * 3");
5222        let Statement::Select(s) = s else {
5223            panic!("expected SELECT")
5224        };
5225        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5226            panic!()
5227        };
5228        assert_eq!(
5229            expr,
5230            &Expr::Binary {
5231                lhs: Box::new(Expr::Binary {
5232                    lhs: Box::new(lit_int(1)),
5233                    op: BinOp::Add,
5234                    rhs: Box::new(lit_int(2)),
5235                }),
5236                op: BinOp::Mul,
5237                rhs: Box::new(lit_int(3)),
5238            }
5239        );
5240    }
5241
5242    #[test]
5243    fn not_binds_below_comparison() {
5244        // `NOT a = 1` should parse as `NOT (a = 1)`.
5245        let s = parse("SELECT NOT a = 1 FROM t");
5246        let Statement::Select(s) = s else {
5247            panic!("expected SELECT")
5248        };
5249        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5250            panic!()
5251        };
5252        assert_eq!(
5253            expr,
5254            &Expr::Unary {
5255                op: UnOp::Not,
5256                expr: Box::new(Expr::Binary {
5257                    lhs: Box::new(col("a")),
5258                    op: BinOp::Eq,
5259                    rhs: Box::new(lit_int(1)),
5260                }),
5261            }
5262        );
5263    }
5264
5265    #[test]
5266    fn unary_minus_binds_above_multiplication() {
5267        // `-a * 2` should be `(-a) * 2`.
5268        let s = parse("SELECT -a * 2 FROM t");
5269        let Statement::Select(s) = s else {
5270            panic!("expected SELECT")
5271        };
5272        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5273            panic!()
5274        };
5275        assert_eq!(
5276            expr,
5277            &Expr::Binary {
5278                lhs: Box::new(Expr::Unary {
5279                    op: UnOp::Neg,
5280                    expr: Box::new(col("a")),
5281                }),
5282                op: BinOp::Mul,
5283                rhs: Box::new(lit_int(2)),
5284            }
5285        );
5286    }
5287
5288    #[test]
5289    fn qualified_column() {
5290        let s = parse("SELECT t.col FROM t");
5291        let Statement::Select(s) = s else {
5292            panic!("expected SELECT")
5293        };
5294        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5295            panic!()
5296        };
5297        assert_eq!(
5298            expr,
5299            &Expr::Column(ColumnName {
5300                qualifier: Some("t".into()),
5301                name: "col".into()
5302            })
5303        );
5304    }
5305
5306    #[test]
5307    fn select_item_alias_with_as() {
5308        let s = parse("SELECT a AS y FROM t");
5309        let Statement::Select(s) = s else {
5310            panic!("expected SELECT")
5311        };
5312        let SelectItem::Expr { alias, .. } = &s.items[0] else {
5313            panic!()
5314        };
5315        assert_eq!(alias.as_deref(), Some("y"));
5316    }
5317
5318    #[test]
5319    fn trailing_semicolon_accepted() {
5320        let s = parse("SELECT 1;");
5321        let Statement::Select(s) = s else {
5322            panic!("expected SELECT")
5323        };
5324        assert_eq!(s.items.len(), 1);
5325    }
5326
5327    #[test]
5328    fn boolean_chain_with_and_or_not() {
5329        // (NOT a) OR (b AND (NOT c))
5330        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
5331        let Statement::Select(s) = s else {
5332            panic!("expected SELECT")
5333        };
5334        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5335            panic!()
5336        };
5337        let expected = Expr::Binary {
5338            lhs: Box::new(Expr::Unary {
5339                op: UnOp::Not,
5340                expr: Box::new(col("a")),
5341            }),
5342            op: BinOp::Or,
5343            rhs: Box::new(Expr::Binary {
5344                lhs: Box::new(col("b")),
5345                op: BinOp::And,
5346                rhs: Box::new(Expr::Unary {
5347                    op: UnOp::Not,
5348                    expr: Box::new(col("c")),
5349                }),
5350            }),
5351        };
5352        assert_eq!(expr, &expected);
5353    }
5354
5355    #[test]
5356    fn empty_input_errors() {
5357        let err = parse_statement("").unwrap_err();
5358        assert!(err.message.contains("SELECT"));
5359    }
5360
5361    #[test]
5362    fn unmatched_paren_errors() {
5363        assert!(parse_statement("SELECT (1 + 2").is_err());
5364    }
5365
5366    #[test]
5367    fn display_round_trip_simple_select() {
5368        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
5369        let text = original.to_string();
5370        let again = parse_statement(&text).expect("re-parse");
5371        assert_eq!(original, again);
5372    }
5373
5374    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
5375
5376    #[test]
5377    fn create_table_single_column() {
5378        let s = parse("CREATE TABLE foo (a INT)");
5379        let Statement::CreateTable(c) = s else {
5380            panic!("expected CreateTable")
5381        };
5382        assert_eq!(c.name, "foo");
5383        assert_eq!(c.columns.len(), 1);
5384        assert_eq!(c.columns[0].name, "a");
5385        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
5386        assert!(c.columns[0].nullable);
5387    }
5388
5389    #[test]
5390    fn create_table_multi_column_with_not_null_mix() {
5391        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
5392        let Statement::CreateTable(c) = s else {
5393            panic!()
5394        };
5395        assert_eq!(c.columns.len(), 4);
5396        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
5397        assert!(!c.columns[0].nullable);
5398        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
5399        assert!(c.columns[1].nullable);
5400        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
5401        assert!(!c.columns[2].nullable);
5402        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
5403    }
5404
5405    #[test]
5406    fn create_table_bigint_supported() {
5407        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
5408        let Statement::CreateTable(c) = s else {
5409            panic!()
5410        };
5411        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
5412    }
5413
5414    #[test]
5415    fn create_table_vector_default_is_f32() {
5416        let s = parse("CREATE TABLE t (v VECTOR(128))");
5417        let Statement::CreateTable(c) = s else {
5418            panic!()
5419        };
5420        assert_eq!(
5421            c.columns[0].ty,
5422            ColumnTypeName::Vector {
5423                dim: 128,
5424                encoding: VecEncoding::F32,
5425            },
5426        );
5427    }
5428
5429    #[test]
5430    fn create_table_vector_using_sq8() {
5431        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
5432        // Case-insensitive on both `USING` and the encoding name.
5433        for sql in [
5434            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
5435            "CREATE TABLE t (v VECTOR(128) using sq8)",
5436        ] {
5437            let s = parse(sql);
5438            let Statement::CreateTable(c) = s else {
5439                panic!()
5440            };
5441            assert_eq!(
5442                c.columns[0].ty,
5443                ColumnTypeName::Vector {
5444                    dim: 128,
5445                    encoding: VecEncoding::Sq8,
5446                },
5447                "{sql}",
5448            );
5449        }
5450    }
5451
5452    #[test]
5453    fn create_table_vector_using_unknown_errors() {
5454        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
5455        assert!(
5456            err.message.contains("unknown vector encoding"),
5457            "got: {}",
5458            err.message
5459        );
5460    }
5461
5462    #[test]
5463    fn vector_using_sq8_display_roundtrips() {
5464        // The Display impl must produce text that re-parses to the
5465        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
5466        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
5467        let Statement::CreateTable(c) = s else {
5468            panic!()
5469        };
5470        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
5471    }
5472
5473    #[test]
5474    fn parser_recognises_placeholders() {
5475        use crate::ast::{Expr, SelectItem, Statement};
5476        // $N in expression position parses as Expr::Placeholder(N).
5477        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
5478        let Statement::Select(sel) = s else { panic!() };
5479        assert!(matches!(
5480            sel.items[0],
5481            SelectItem::Expr {
5482                expr: Expr::Placeholder(1),
5483                alias: None
5484            }
5485        ));
5486        // $2 + 1
5487        let SelectItem::Expr {
5488            expr: Expr::Binary { lhs, rhs, .. },
5489            ..
5490        } = &sel.items[1]
5491        else {
5492            panic!()
5493        };
5494        assert!(matches!(**lhs, Expr::Placeholder(2)));
5495        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
5496        // WHERE x = $3
5497        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
5498            panic!()
5499        };
5500        assert!(matches!(**rhs, Expr::Placeholder(3)));
5501    }
5502
5503    #[test]
5504    fn parser_rejects_dollar_zero() {
5505        // $0 is not valid in PG; the lexer rejects it.
5506        assert!(parse_statement("SELECT $0").is_err());
5507    }
5508
5509    #[test]
5510    fn placeholder_display_roundtrips() {
5511        // The Display impl must produce text that re-lexes to the
5512        // same Placeholder token.
5513        let s = parse("SELECT $42 FROM t");
5514        let printed = s.to_string();
5515        assert!(printed.contains("$42"));
5516        let again = parse(&printed);
5517        assert_eq!(s, again);
5518    }
5519
5520    #[test]
5521    fn alter_index_rebuild_bare() {
5522        use crate::ast::{AlterIndexTarget, Statement};
5523        let s = parse("ALTER INDEX my_idx REBUILD");
5524        let Statement::AlterIndex(a) = s else {
5525            panic!("expected AlterIndex, got {s:?}")
5526        };
5527        assert_eq!(a.name, "my_idx");
5528        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
5529    }
5530
5531    #[test]
5532    fn alter_index_rebuild_with_encoding() {
5533        use crate::ast::{AlterIndexTarget, Statement};
5534        for (sql, want) in [
5535            (
5536                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
5537                VecEncoding::F32,
5538            ),
5539            (
5540                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
5541                VecEncoding::Sq8,
5542            ),
5543            (
5544                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5545                VecEncoding::F16,
5546            ),
5547        ] {
5548            let s = parse(sql);
5549            let Statement::AlterIndex(a) = s else {
5550                panic!("{sql}: expected AlterIndex")
5551            };
5552            assert_eq!(a.name, "my_idx");
5553            assert_eq!(
5554                a.target,
5555                AlterIndexTarget::Rebuild {
5556                    encoding: Some(want)
5557                },
5558                "{sql}"
5559            );
5560        }
5561    }
5562
5563    #[test]
5564    fn alter_index_rebuild_unknown_encoding_errors() {
5565        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
5566        assert!(
5567            err.message.contains("unknown vector encoding"),
5568            "got: {}",
5569            err.message
5570        );
5571    }
5572
5573    #[test]
5574    fn alter_index_rebuild_display_roundtrips() {
5575        for (input, want) in [
5576            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
5577            (
5578                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
5579                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
5580            ),
5581            (
5582                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5583                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5584            ),
5585        ] {
5586            let s = parse(input);
5587            assert_eq!(s.to_string(), want);
5588        }
5589    }
5590
5591    #[test]
5592    fn create_table_unknown_type_errors() {
5593        // v4.9: JSON is now real; pick an actually unsupported keyword
5594        // (XML never landed and isn't planned).
5595        let err = parse_statement("CREATE TABLE x (a xml)").unwrap_err();
5596        assert!(err.message.contains("unsupported column type"));
5597    }
5598
5599    #[test]
5600    fn create_table_missing_table_keyword_errors() {
5601        assert!(parse_statement("CREATE x (a INT)").is_err());
5602    }
5603
5604    #[test]
5605    fn insert_single_value() {
5606        let s = parse("INSERT INTO foo VALUES (42)");
5607        let Statement::Insert(i) = s else {
5608            panic!("expected Insert")
5609        };
5610        assert_eq!(i.table, "foo");
5611        assert_eq!(i.rows.len(), 1);
5612        assert_eq!(i.rows[0].len(), 1);
5613        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
5614    }
5615
5616    #[test]
5617    fn insert_multi_value_with_mixed_literals() {
5618        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
5619        let Statement::Insert(i) = s else { panic!() };
5620        assert_eq!(i.rows.len(), 1);
5621        assert_eq!(i.rows[0].len(), 5);
5622    }
5623
5624    #[test]
5625    fn insert_missing_into_errors() {
5626        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
5627    }
5628
5629    #[test]
5630    fn create_table_round_trip() {
5631        let original =
5632            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
5633        let text = original.to_string();
5634        let again = parse_statement(&text).expect("re-parse");
5635        assert_eq!(original, again);
5636    }
5637
5638    #[test]
5639    fn insert_round_trip_with_negation_and_string() {
5640        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
5641        let text = original.to_string();
5642        let again = parse_statement(&text).expect("re-parse");
5643        assert_eq!(original, again);
5644    }
5645
5646    #[test]
5647    fn unknown_keyword_at_statement_start_errors() {
5648        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
5649        // the top-level dispatch still has no branch to take.
5650        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
5651        assert!(err.message.contains("expected SELECT"));
5652    }
5653
5654    // --- v0.8 CREATE INDEX --------------------------------------------------
5655
5656    #[test]
5657    fn create_index_basic() {
5658        let s = parse("CREATE INDEX idx_id ON users (id)");
5659        let Statement::CreateIndex(c) = s else {
5660            panic!("expected CreateIndex")
5661        };
5662        assert_eq!(c.name, "idx_id");
5663        assert_eq!(c.table, "users");
5664        assert_eq!(c.column, "id");
5665    }
5666
5667    #[test]
5668    fn create_index_missing_on_errors() {
5669        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
5670    }
5671
5672    #[test]
5673    fn create_index_missing_paren_errors() {
5674        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
5675    }
5676
5677    #[test]
5678    fn create_index_round_trip() {
5679        let original = parse("CREATE INDEX by_name ON users (name)");
5680        let again = parse_statement(&original.to_string()).unwrap();
5681        assert_eq!(original, again);
5682    }
5683
5684    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
5685
5686    #[test]
5687    fn create_unique_index_basic() {
5688        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
5689        let Statement::CreateIndex(c) = s else {
5690            panic!("expected CreateIndex");
5691        };
5692        assert!(c.is_unique);
5693        assert_eq!(c.column, "a");
5694        assert!(c.partial_predicate.is_none());
5695    }
5696
5697    #[test]
5698    fn create_unique_index_partial() {
5699        // mailrs's email_templates "one default per user" shape.
5700        let s = parse(
5701            "CREATE UNIQUE INDEX idx_email_templates_user_default \
5702             ON email_templates (user_address) WHERE is_default = true",
5703        );
5704        let Statement::CreateIndex(c) = s else {
5705            panic!("expected CreateIndex");
5706        };
5707        assert!(c.is_unique);
5708        assert_eq!(c.table, "email_templates");
5709        assert_eq!(c.column, "user_address");
5710        assert!(c.partial_predicate.is_some());
5711    }
5712
5713    #[test]
5714    fn create_unique_index_composite_with_predicate() {
5715        // mailrs's calendar_events instance: composite columns.
5716        let s = parse(
5717            "CREATE UNIQUE INDEX uq_calendar_events_instance \
5718             ON calendar_events (calendar_id, uid, recurrence_id) \
5719             WHERE recurrence_id IS NOT NULL",
5720        );
5721        let Statement::CreateIndex(c) = s else {
5722            panic!("expected CreateIndex");
5723        };
5724        assert!(c.is_unique);
5725        assert_eq!(c.column, "calendar_id");
5726        assert_eq!(
5727            c.extra_columns,
5728            vec!["uid".to_string(), "recurrence_id".to_string()]
5729        );
5730        assert!(c.partial_predicate.is_some());
5731    }
5732
5733    #[test]
5734    fn create_unique_index_using_btree_ok() {
5735        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
5736        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
5737    }
5738
5739    #[test]
5740    fn create_unique_index_using_hnsw_rejected() {
5741        let err =
5742            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
5743        assert!(err.message.contains("UNIQUE"), "{}", err.message);
5744    }
5745
5746    #[test]
5747    fn create_unique_index_round_trip() {
5748        let original = parse(
5749            "CREATE UNIQUE INDEX uq_calendar_events_master \
5750             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
5751        );
5752        let again = parse_statement(&original.to_string()).unwrap();
5753        assert_eq!(original, again);
5754    }
5755
5756    #[test]
5757    fn create_unique_without_index_errors() {
5758        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
5759        assert!(err.message.contains("INDEX"), "{}", err.message);
5760    }
5761
5762    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
5763
5764    #[test]
5765    fn create_table_bytea_column() {
5766        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
5767        let Statement::CreateTable(c) = s else {
5768            panic!("expected CreateTable");
5769        };
5770        assert_eq!(c.columns.len(), 2);
5771        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
5772        assert!(!c.columns[1].nullable);
5773    }
5774
5775    #[test]
5776    fn create_table_bytes_alias_column() {
5777        let s = parse("CREATE TABLE t (blob BYTES)");
5778        let Statement::CreateTable(c) = s else {
5779            panic!("expected CreateTable");
5780        };
5781        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
5782    }
5783
5784    #[test]
5785    fn bytea_round_trip_display() {
5786        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
5787        let again = parse_statement(&original.to_string()).unwrap();
5788        assert_eq!(original, again);
5789    }
5790
5791    // --- v0.9 transactions -------------------------------------------------
5792
5793    #[test]
5794    fn begin_commit_rollback_parse_as_unit_variants() {
5795        assert_eq!(parse("BEGIN"), Statement::Begin);
5796        assert_eq!(parse("COMMIT"), Statement::Commit);
5797        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
5798        // Trailing semicolons accepted too.
5799        assert_eq!(parse("BEGIN;"), Statement::Begin);
5800    }
5801
5802    // --- v1.2: pgvector distance ops + ::vector cast --------------------
5803
5804    #[test]
5805    fn inner_product_binop_parses() {
5806        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
5807        let Statement::Select(s) = s else { panic!() };
5808        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5809            panic!()
5810        };
5811        assert!(matches!(
5812            expr,
5813            Expr::Binary {
5814                op: BinOp::InnerProduct,
5815                ..
5816            }
5817        ));
5818    }
5819
5820    #[test]
5821    fn cosine_distance_binop_parses() {
5822        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
5823        let Statement::Select(s) = s else { panic!() };
5824        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5825            panic!()
5826        };
5827        assert!(matches!(
5828            expr,
5829            Expr::Binary {
5830                op: BinOp::CosineDistance,
5831                ..
5832            }
5833        ));
5834    }
5835
5836    #[test]
5837    fn vector_cast_postfix_wraps_string_literal() {
5838        let s = parse("SELECT '[1,2,3]'::vector FROM t");
5839        let Statement::Select(s) = s else { panic!() };
5840        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5841            panic!()
5842        };
5843        assert!(matches!(
5844            expr,
5845            Expr::Cast {
5846                target: CastTarget::Vector,
5847                ..
5848            }
5849        ));
5850    }
5851
5852    #[test]
5853    fn unsupported_cast_target_errors() {
5854        // `::numeric` isn't in the v1.3 cast target set.
5855        let err = parse_statement("SELECT 1::numeric FROM t").unwrap_err();
5856        assert!(err.message.contains("unsupported cast target"));
5857    }
5858
5859    #[test]
5860    fn tx_statements_round_trip() {
5861        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
5862            let original = parse(q);
5863            let again = parse_statement(&original.to_string()).unwrap();
5864            assert_eq!(original, again);
5865        }
5866    }
5867
5868    #[test]
5869    fn interval_text_parsing_units() {
5870        // Single unit.
5871        assert_eq!(parse_interval_text("1 day"), Some((0, 86_400_000_000)));
5872        assert_eq!(parse_interval_text("1 second"), Some((0, 1_000_000)));
5873        assert_eq!(parse_interval_text("1 month"), Some((1, 0)));
5874        assert_eq!(parse_interval_text("2 years"), Some((24, 0)));
5875        // Compound spans accumulate.
5876        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0)));
5877        assert_eq!(
5878            parse_interval_text("1 day 2 hours"),
5879            Some((0, 86_400_000_000 + 7_200_000_000))
5880        );
5881        // Negative numbers carry through.
5882        assert_eq!(parse_interval_text("-1 day"), Some((0, -86_400_000_000)));
5883        // Bad shapes return None.
5884        assert_eq!(parse_interval_text(""), None);
5885        assert_eq!(parse_interval_text("garbage"), None);
5886        assert_eq!(parse_interval_text("1 fortnight"), None);
5887        assert_eq!(parse_interval_text("1"), None);
5888    }
5889
5890    #[test]
5891    fn interval_literal_roundtrips_via_display() {
5892        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
5893        let s = parsed.to_string();
5894        // Display preserves the original text verbatim.
5895        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
5896        // And re-parsing yields a structurally equal statement.
5897        let again = parse_statement(&s).unwrap();
5898        assert_eq!(parsed, again);
5899    }
5900
5901    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
5902
5903    #[test]
5904    fn parser_recognises_create_publication_bare() {
5905        let s = parse("CREATE PUBLICATION pub_a");
5906        let Statement::CreatePublication(p) = s else {
5907            panic!("expected CreatePublication, got {s:?}")
5908        };
5909        assert_eq!(p.name, "pub_a");
5910        assert_eq!(p.scope, PublicationScope::AllTables);
5911    }
5912
5913    #[test]
5914    fn parser_recognises_create_publication_for_all_tables() {
5915        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
5916        let Statement::CreatePublication(p) = s else {
5917            panic!("expected CreatePublication, got {s:?}")
5918        };
5919        assert_eq!(p.name, "pub_a");
5920        assert_eq!(p.scope, PublicationScope::AllTables);
5921    }
5922
5923    #[test]
5924    fn parser_recognises_drop_publication() {
5925        let s = parse("DROP PUBLICATION pub_a");
5926        let Statement::DropPublication(name) = s else {
5927            panic!("expected DropPublication, got {s:?}")
5928        };
5929        assert_eq!(name, "pub_a");
5930    }
5931
5932    #[test]
5933    fn parser_recognises_for_table_list() {
5934        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
5935        let Statement::CreatePublication(p) = s else {
5936            panic!("expected CreatePublication, got {s:?}")
5937        };
5938        assert_eq!(p.name, "pub_a");
5939        let PublicationScope::ForTables(ts) = p.scope else {
5940            panic!("expected ForTables scope")
5941        };
5942        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
5943    }
5944
5945    #[test]
5946    fn parser_recognises_for_tables_plural() {
5947        // PG 19 accepts both `FOR TABLE` and `FOR TABLES` — match.
5948        let s = parse("CREATE PUBLICATION pub_a FOR TABLES t1, t2");
5949        let Statement::CreatePublication(p) = s else {
5950            panic!("expected CreatePublication, got {s:?}")
5951        };
5952        let PublicationScope::ForTables(ts) = p.scope else {
5953            panic!("expected ForTables")
5954        };
5955        assert_eq!(ts, alloc::vec!["t1", "t2"]);
5956    }
5957
5958    #[test]
5959    fn parser_recognises_for_all_tables_except_list() {
5960        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
5961        let Statement::CreatePublication(p) = s else {
5962            panic!()
5963        };
5964        let PublicationScope::AllTablesExcept(ts) = p.scope else {
5965            panic!("expected AllTablesExcept")
5966        };
5967        assert_eq!(ts, alloc::vec!["t1", "t2"]);
5968    }
5969
5970    #[test]
5971    fn parser_rejects_for_table_with_empty_list() {
5972        // `FOR TABLE` with nothing after is a parse error.
5973        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
5974            .expect_err("must error on empty list");
5975        // No specific message asserted — the call falls through to
5976        // expect_ident_like which yields "expected identifier, got …".
5977        assert!(!err.message.is_empty());
5978    }
5979
5980    #[test]
5981    fn parser_recognises_show_publications() {
5982        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
5983        // bare ident in this position, NOT a reserved keyword.
5984        let s = parse("SHOW PUBLICATIONS");
5985        assert!(matches!(s, Statement::ShowPublications));
5986    }
5987
5988    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
5989
5990    #[test]
5991    fn parser_recognises_create_subscription_single_publication() {
5992        let s = parse(
5993            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
5994        );
5995        let Statement::CreateSubscription(c) = s else {
5996            panic!("expected CreateSubscription, got {s:?}")
5997        };
5998        assert_eq!(c.name, "sub_a");
5999        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
6000        assert_eq!(c.publications, alloc::vec!["pub_a"]);
6001    }
6002
6003    #[test]
6004    fn parser_recognises_create_subscription_multi_publication() {
6005        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
6006        let Statement::CreateSubscription(c) = s else {
6007            panic!()
6008        };
6009        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
6010    }
6011
6012    #[test]
6013    fn parser_rejects_create_subscription_missing_connection() {
6014        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
6015            .expect_err("must error on missing CONNECTION");
6016        assert!(err.message.contains("CONNECTION"), "got: {}", err.message);
6017    }
6018
6019    #[test]
6020    fn parser_rejects_create_subscription_missing_publication() {
6021        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
6022            .expect_err("must error on missing PUBLICATION");
6023        assert!(err.message.contains("PUBLICATION"), "got: {}", err.message);
6024    }
6025
6026    #[test]
6027    fn parser_recognises_drop_subscription() {
6028        let s = parse("DROP SUBSCRIPTION sub_a");
6029        let Statement::DropSubscription(name) = s else {
6030            panic!("expected DropSubscription, got {s:?}")
6031        };
6032        assert_eq!(name, "sub_a");
6033    }
6034
6035    #[test]
6036    fn parser_recognises_show_subscriptions() {
6037        let s = parse("SHOW SUBSCRIPTIONS");
6038        assert!(matches!(s, Statement::ShowSubscriptions));
6039    }
6040
6041    #[test]
6042    fn parser_recognises_wait_for_wal_position_no_timeout() {
6043        let s = parse("WAIT FOR WAL POSITION 12345");
6044        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
6045            panic!("expected WaitForWalPosition, got {s:?}")
6046        };
6047        assert_eq!(pos, 12345);
6048        assert!(timeout_ms.is_none());
6049    }
6050
6051    #[test]
6052    fn parser_recognises_wait_for_wal_position_with_timeout() {
6053        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
6054        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
6055            panic!()
6056        };
6057        assert_eq!(pos, 67890);
6058        assert_eq!(timeout_ms, Some(5000));
6059    }
6060
6061    #[test]
6062    fn parser_rejects_wait_with_negative_position() {
6063        // The lexer treats `-` as a token; `expect_u64_literal`
6064        // only sees the Integer that follows, so the negative
6065        // arrives as a unary-minus expression at higher levels.
6066        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
6067        // parse error one way or another.
6068        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
6069        assert!(!err.message.is_empty());
6070    }
6071
6072    #[test]
6073    fn parser_recognises_bare_analyze() {
6074        let s = parse("ANALYZE");
6075        assert!(matches!(s, Statement::Analyze(None)));
6076    }
6077
6078    #[test]
6079    fn parser_recognises_analyze_with_table() {
6080        let s = parse("ANALYZE users");
6081        let Statement::Analyze(Some(name)) = s else {
6082            panic!("expected Analyze, got {s:?}")
6083        };
6084        assert_eq!(name, "users");
6085    }
6086
6087    #[test]
6088    fn parser_recognises_analyze_with_quoted_table() {
6089        let s = parse("ANALYZE \"Mixed Case\"");
6090        let Statement::Analyze(Some(name)) = s else {
6091            panic!()
6092        };
6093        assert_eq!(name, "Mixed Case");
6094    }
6095
6096    #[test]
6097    fn parser_rejects_analyze_with_garbage_token() {
6098        let err = parse_statement("ANALYZE 42").expect_err("must error");
6099        assert!(!err.message.is_empty());
6100    }
6101
6102    #[test]
6103    fn analyze_display_roundtrips() {
6104        for sql in ["ANALYZE", "ANALYZE users"] {
6105            let s = parse(sql);
6106            let printed = s.to_string();
6107            let again = parse_statement(&printed)
6108                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6109            assert_eq!(s, again);
6110        }
6111    }
6112
6113    #[test]
6114    fn wait_for_display_roundtrips() {
6115        for sql in [
6116            "WAIT FOR WAL POSITION 12345",
6117            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
6118        ] {
6119            let s = parse(sql);
6120            let printed = s.to_string();
6121            let again = parse_statement(&printed)
6122                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6123            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6124        }
6125    }
6126
6127    #[test]
6128    fn subscription_ddl_display_roundtrips() {
6129        for sql in [
6130            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
6131            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
6132            "DROP SUBSCRIPTION sub_a",
6133            "SHOW SUBSCRIPTIONS",
6134        ] {
6135            let s = parse(sql);
6136            let printed = s.to_string();
6137            let again = parse_statement(&printed)
6138                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6139            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6140        }
6141    }
6142
6143    #[test]
6144    fn parser_drop_dispatches_user_vs_publication() {
6145        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
6146        // tokenises DROP. Both targets must still parse.
6147        let s = parse("DROP USER 'alice'");
6148        let Statement::DropUser(name) = s else {
6149            panic!("expected DropUser, got {s:?}")
6150        };
6151        assert_eq!(name, "alice");
6152        // And DROP PUBLICATION lands the new variant.
6153        let s = parse("DROP PUBLICATION p1");
6154        assert!(matches!(s, Statement::DropPublication(_)));
6155    }
6156
6157    #[test]
6158    fn publication_ddl_display_roundtrips() {
6159        // Every CREATE PUBLICATION variant must Display → parse →
6160        // same AST. v6.1.3 covers all three scope shapes.
6161        for sql in [
6162            "CREATE PUBLICATION pub_a",
6163            "CREATE PUBLICATION pub_a FOR ALL TABLES",
6164            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
6165            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
6166            "DROP PUBLICATION pub_a",
6167            "SHOW PUBLICATIONS",
6168        ] {
6169            let s = parse(sql);
6170            let printed = s.to_string();
6171            let again = parse_statement(&printed)
6172                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6173            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6174        }
6175    }
6176
6177    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
6178
6179    #[test]
6180    fn create_function_returns_trigger_plpgsql_minimal() {
6181        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
6182        let s = parse(sql);
6183        let Statement::CreateFunction(f) = s else {
6184            panic!("expected CreateFunction");
6185        };
6186        assert_eq!(f.name, "noop");
6187        assert!(!f.or_replace);
6188        assert!(f.args.is_empty());
6189        assert!(matches!(f.returns, FunctionReturn::Trigger));
6190        assert_eq!(f.language, "plpgsql");
6191        let FunctionBody::PlPgSql(block) = f.body else {
6192            panic!("expected PlPgSql body");
6193        };
6194        assert_eq!(block.statements.len(), 1);
6195        assert!(matches!(
6196            block.statements[0],
6197            PlPgSqlStmt::Return(ReturnTarget::New)
6198        ));
6199    }
6200
6201    #[test]
6202    fn create_function_or_replace_with_assignment() {
6203        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
6204        // RETURN NEW.
6205        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
6206BEGIN
6207  NEW.search_vector := to_tsvector('english', NEW.subject);
6208  RETURN NEW;
6209END;
6210$$";
6211        let s = parse(sql);
6212        let Statement::CreateFunction(f) = s else {
6213            panic!("expected CreateFunction");
6214        };
6215        assert!(f.or_replace);
6216        let FunctionBody::PlPgSql(block) = &f.body else {
6217            panic!("expected PlPgSql body");
6218        };
6219        assert_eq!(block.statements.len(), 2);
6220        // First statement: NEW.search_vector := to_tsvector(...)
6221        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
6222            panic!("expected Assign as first stmt");
6223        };
6224        match target {
6225            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
6226            other => panic!("expected NEW.col, got {other:?}"),
6227        }
6228        // Second statement: RETURN NEW
6229        assert!(matches!(
6230            block.statements[1],
6231            PlPgSqlStmt::Return(ReturnTarget::New)
6232        ));
6233    }
6234
6235    #[test]
6236    fn create_trigger_after_insert_or_update() {
6237        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
6238        let s = parse(sql);
6239        let Statement::CreateTrigger(t) = s else {
6240            panic!("expected CreateTrigger");
6241        };
6242        assert_eq!(t.name, "tg");
6243        assert_eq!(t.table, "messages");
6244        assert_eq!(t.timing, TriggerTiming::After);
6245        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
6246        assert_eq!(t.for_each, TriggerForEach::Row);
6247        assert_eq!(t.function, "update_sv");
6248    }
6249
6250    #[test]
6251    fn create_trigger_before_delete_execute_procedure_alias() {
6252        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
6253        let sql =
6254            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
6255        let s = parse(sql);
6256        let Statement::CreateTrigger(t) = s else {
6257            panic!("expected CreateTrigger");
6258        };
6259        assert_eq!(t.timing, TriggerTiming::Before);
6260        assert_eq!(t.events, vec![TriggerEvent::Delete]);
6261    }
6262
6263    #[test]
6264    fn drop_trigger_if_exists_round_trips() {
6265        // No parser support for DROP TRIGGER yet — added in v7.12.5
6266        // alongside the broader DROP …{IF EXISTS} cleanup. The
6267        // AST + Display impls are in place so we round-trip via
6268        // construction:
6269        let s = Statement::DropTrigger {
6270            name: "tg".into(),
6271            table: "messages".into(),
6272            if_exists: true,
6273        };
6274        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
6275    }
6276
6277    #[test]
6278    fn trigger_ddl_display_roundtrips_through_parser() {
6279        // CREATE TRIGGER + its referenced CREATE FUNCTION must
6280        // Display → parse → same AST (modulo PL/pgSQL body
6281        // formatting which is parser-canonicalised).
6282        for sql in [
6283            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
6284            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
6285        ] {
6286            let s = parse(sql);
6287            let printed = s.to_string();
6288            let again = parse_statement(&printed)
6289                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6290            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6291        }
6292    }
6293}