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    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
1774        let table_name = self.expect_ident_like()?;
1775        // v7.6.8 — dispatch on the next keyword: SET / ADD / DROP.
1776        // SET kept identical to v6.7.x. ADD / DROP CONSTRAINT routes
1777        // to FK installation / removal.
1778        match self.peek() {
1779            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
1780                self.advance();
1781                let setting = self.expect_ident_like()?;
1782                if !setting.eq_ignore_ascii_case("hot_tier_bytes") {
1783                    return Err(self.err(alloc::format!(
1784                        "ALTER TABLE SET: unknown setting {setting:?}; supported: hot_tier_bytes"
1785                    )));
1786                }
1787                if !matches!(self.peek(), Token::Eq) {
1788                    return Err(self.err(alloc::format!(
1789                        "expected '=' after hot_tier_bytes, got {:?}",
1790                        self.peek()
1791                    )));
1792                }
1793                self.advance();
1794                let n = self.expect_u64_literal()?;
1795                Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1796                    name: table_name,
1797                    target: crate::ast::AlterTableTarget::SetHotTierBytes(n),
1798                }))
1799            }
1800            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
1801                self.advance();
1802                // v7.13.0 — dispatch on the next token. `CONSTRAINT` /
1803                // `FOREIGN` route to the FK arm (existing v7.6.8
1804                // behaviour). `COLUMN` and any bare identifier that
1805                // isn't one of those keywords route to ADD COLUMN.
1806                // mailrs round-5 G1.
1807                let is_fk = matches!(
1808                    self.peek(),
1809                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
1810                        || s.eq_ignore_ascii_case("foreign")
1811                );
1812                if is_fk {
1813                    let fk = self.parse_table_level_fk()?;
1814                    return Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1815                        name: table_name,
1816                        target: crate::ast::AlterTableTarget::AddForeignKey(fk),
1817                    }));
1818                }
1819                // Optional `COLUMN` keyword (PG accepts either form;
1820                // ADD COLUMN is the canonical spelling).
1821                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
1822                    self.advance();
1823                }
1824                // Optional `IF NOT EXISTS` — skipped silently when the
1825                // column already exists at engine time.
1826                let mut if_not_exists = false;
1827                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
1828                    self.advance();
1829                    if !matches!(self.peek(), Token::Not) {
1830                        return Err(self.err(alloc::format!(
1831                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
1832                            self.peek()
1833                        )));
1834                    }
1835                    self.advance();
1836                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
1837                        return Err(self.err(alloc::format!(
1838                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
1839                            self.peek()
1840                        )));
1841                    }
1842                    self.advance();
1843                    if_not_exists = true;
1844                }
1845                let column = self.parse_column_def()?;
1846                Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1847                    name: table_name,
1848                    target: crate::ast::AlterTableTarget::AddColumn {
1849                        column,
1850                        if_not_exists,
1851                    },
1852                }))
1853            }
1854            Token::Drop => {
1855                self.advance();
1856                match self.advance() {
1857                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {}
1858                    other => {
1859                        return Err(self.err(alloc::format!(
1860                            "expected CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
1861                        )));
1862                    }
1863                }
1864                let cname = self.expect_ident_like()?;
1865                Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1866                    name: table_name,
1867                    target: crate::ast::AlterTableTarget::DropForeignKey(cname),
1868                }))
1869            }
1870            // v7.13.0 — `ALTER TABLE t ALTER COLUMN <c> TYPE <ty>
1871            // [USING <expr>]` (mailrs round-5 G8).
1872            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
1873                self.advance();
1874                // Optional `COLUMN` keyword (PG-canonical).
1875                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
1876                    self.advance();
1877                }
1878                let col_name = self.expect_ident_like()?;
1879                // Required `TYPE` keyword.
1880                match self.peek() {
1881                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
1882                        self.advance();
1883                    }
1884                    other => {
1885                        return Err(self.err(alloc::format!(
1886                            "expected TYPE after ALTER COLUMN <name>, got {other:?}"
1887                        )));
1888                    }
1889                }
1890                let new_type = self.parse_column_type_name()?;
1891                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
1892                {
1893                    self.advance();
1894                    Some(self.parse_expr(0)?)
1895                } else {
1896                    None
1897                };
1898                Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
1899                    name: table_name,
1900                    target: crate::ast::AlterTableTarget::AlterColumnType {
1901                        column: col_name,
1902                        new_type,
1903                        using,
1904                    },
1905                }))
1906            }
1907            other => Err(self.err(alloc::format!(
1908                "expected SET / ADD / DROP / ALTER in ALTER TABLE, got {other:?}"
1909            ))),
1910        }
1911    }
1912
1913    /// Consume a bare ident if its lowercase matches `kw`, else err.
1914    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
1915        match self.advance() {
1916            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
1917            other => Err(ParseError {
1918                message: format!("expected {kw:?}, got {other:?}"),
1919                token_pos: self.pos.saturating_sub(1),
1920            }),
1921        }
1922    }
1923
1924    /// Accept either a quoted identifier (`"foo"`) or a quoted string
1925    /// literal (`'foo'`) — same shape used by CREATE USER for the
1926    /// username slot.
1927    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
1928        match self.advance() {
1929            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
1930            other => Err(ParseError {
1931                message: format!("expected identifier or string, got {other:?}"),
1932                token_pos: self.pos.saturating_sub(1),
1933            }),
1934        }
1935    }
1936
1937    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
1938        match self.advance() {
1939            Token::String(s) => Ok(s),
1940            other => Err(ParseError {
1941                message: format!("expected quoted string, got {other:?}"),
1942                token_pos: self.pos.saturating_sub(1),
1943            }),
1944        }
1945    }
1946
1947    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
1948        // Caller dispatches on Token::Select; the inner helper handles
1949        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
1950        // get a fresh bare-select parse and may not have their own ORDER
1951        // BY / LIMIT.
1952        let mut head = self.parse_bare_select()?;
1953        while matches!(self.peek(), Token::Union) {
1954            self.advance();
1955            let kind = if matches!(self.peek(), Token::All) {
1956                self.advance();
1957                UnionKind::All
1958            } else {
1959                UnionKind::Distinct
1960            };
1961            let peer = self.parse_bare_select()?;
1962            head.unions.push((kind, peer));
1963        }
1964        head.order_by = if matches!(self.peek(), Token::Order) {
1965            self.advance();
1966            if !matches!(self.peek(), Token::By) {
1967                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
1968            }
1969            self.advance();
1970            // v6.4.0 — multi-key ORDER BY. Loop over comma-separated
1971            // `<expr> [ASC|DESC]` items.
1972            let mut keys = Vec::new();
1973            loop {
1974                let expr = self.parse_expr(0)?;
1975                let desc = if matches!(self.peek(), Token::Desc) {
1976                    self.advance();
1977                    true
1978                } else if matches!(self.peek(), Token::Asc) {
1979                    self.advance();
1980                    false
1981                } else {
1982                    false
1983                };
1984                keys.push(OrderBy { expr, desc });
1985                if matches!(self.peek(), Token::Comma) {
1986                    self.advance();
1987                } else {
1988                    break;
1989                }
1990            }
1991            keys
1992        } else {
1993            Vec::new()
1994        };
1995        head.limit = if matches!(self.peek(), Token::Limit) {
1996            self.advance();
1997            Some(self.parse_limit_expr("LIMIT")?)
1998        } else {
1999            None
2000        };
2001        head.offset = if matches!(self.peek(), Token::Offset) {
2002            self.advance();
2003            Some(self.parse_limit_expr("OFFSET")?)
2004        } else {
2005            None
2006        };
2007        Ok(Statement::Select(head))
2008    }
2009
2010    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
2011    /// Bind value gets resolved during prepared-statement Execute;
2012    /// the Pratt expression parser would over-accept here (e.g.
2013    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
2014    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
2015        match self.advance() {
2016            Token::Integer(n) if n >= 0 => u32::try_from(n)
2017                .map(crate::ast::LimitExpr::Literal)
2018                .map_err(|_| ParseError {
2019                    message: alloc::format!("{label} value too large: {n}"),
2020                    token_pos: self.pos.saturating_sub(1),
2021                }),
2022            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
2023            other => Err(ParseError {
2024                message: alloc::format!(
2025                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
2026                ),
2027                token_pos: self.pos.saturating_sub(1),
2028            }),
2029        }
2030    }
2031
2032    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
2033    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
2034    /// `unions` empty and `order_by` / `limit` `None`; the top-level
2035    /// `parse_select_stmt` is responsible for filling those in.
2036    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
2037        if !matches!(self.peek(), Token::Select) {
2038            return Err(self.err(format!(
2039                "expected SELECT to start a query block, got {:?}",
2040                self.peek()
2041            )));
2042        }
2043        self.advance();
2044        let distinct = if matches!(self.peek(), Token::Distinct) {
2045            self.advance();
2046            true
2047        } else {
2048            false
2049        };
2050        let items = self.parse_select_list()?;
2051        let from = if matches!(self.peek(), Token::From) {
2052            self.advance();
2053            Some(self.parse_from_clause()?)
2054        } else {
2055            None
2056        };
2057        let where_ = if matches!(self.peek(), Token::Where) {
2058            self.advance();
2059            Some(self.parse_expr(0)?)
2060        } else {
2061            None
2062        };
2063        let mut group_by_all = false;
2064        let group_by = if matches!(self.peek(), Token::Group) {
2065            self.advance();
2066            if !matches!(self.peek(), Token::By) {
2067                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
2068            }
2069            self.advance();
2070            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
2071            // every non-aggregate SELECT-list item later.
2072            if matches!(self.peek(), Token::All) {
2073                self.advance();
2074                group_by_all = true;
2075                None
2076            } else {
2077                let mut groups = Vec::new();
2078                loop {
2079                    groups.push(self.parse_expr(0)?);
2080                    if matches!(self.peek(), Token::Comma) {
2081                        self.advance();
2082                    } else {
2083                        break;
2084                    }
2085                }
2086                Some(groups)
2087            }
2088        } else {
2089            None
2090        };
2091        let having = if matches!(self.peek(), Token::Having) {
2092            self.advance();
2093            Some(self.parse_expr(0)?)
2094        } else {
2095            None
2096        };
2097        Ok(SelectStatement {
2098            ctes: Vec::new(),
2099            distinct,
2100            items,
2101            from,
2102            where_,
2103            group_by,
2104            group_by_all,
2105            having,
2106            unions: Vec::new(),
2107            order_by: Vec::new(),
2108            limit: None,
2109            offset: None,
2110        })
2111    }
2112
2113    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
2114        // Caller already consumed CREATE; we're sitting on TABLE.
2115        debug_assert!(matches!(self.peek(), Token::Table));
2116        self.advance();
2117        let if_not_exists = self.consume_if_not_exists();
2118        let name = self.expect_ident_like()?;
2119        if !matches!(self.peek(), Token::LParen) {
2120            return Err(self.err(format!(
2121                "expected '(' after table name, got {:?}",
2122                self.peek()
2123            )));
2124        }
2125        self.advance();
2126        let mut columns = Vec::new();
2127        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
2128        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
2129        loop {
2130            // v7.6.0 / v7.9.18 — distinguish table-level constraint
2131            // clauses from column definitions. Constraints start
2132            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
2133            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
2134            // a column.
2135            if self.peek_table_level_pk_start() {
2136                table_constraints.push(self.parse_table_level_primary_key()?);
2137            } else if self.peek_table_level_unique_start() {
2138                table_constraints.push(self.parse_table_level_unique()?);
2139            } else if self.peek_table_level_check_start() {
2140                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
2141                table_constraints.push(self.parse_table_level_check()?);
2142            } else if self.peek_constraint_or_fk_start() {
2143                foreign_keys.push(self.parse_table_level_fk()?);
2144            } else {
2145                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
2146                // v7.13.0 — fold inline UNIQUE / CHECK column
2147                // constraints into table-level entries so the
2148                // engine path stays uniform.
2149                if col.is_unique {
2150                    table_constraints.push(crate::ast::TableConstraint::Unique {
2151                        name: None,
2152                        columns: alloc::vec![col.name.clone()],
2153                        nulls_not_distinct: false,
2154                    });
2155                }
2156                if let Some(check_expr) = col.check.clone() {
2157                    table_constraints.push(crate::ast::TableConstraint::Check {
2158                        name: None,
2159                        expr: check_expr,
2160                    });
2161                }
2162                columns.push(col);
2163                if let Some(fk) = col_level_fk {
2164                    foreign_keys.push(fk);
2165                }
2166            }
2167            match self.peek() {
2168                Token::Comma => {
2169                    self.advance();
2170                }
2171                Token::RParen => {
2172                    self.advance();
2173                    break;
2174                }
2175                other => {
2176                    return Err(
2177                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
2178                    );
2179                }
2180            }
2181        }
2182        if columns.is_empty() {
2183            return Err(self.err("CREATE TABLE requires at least one column".into()));
2184        }
2185        Ok(Statement::CreateTable(CreateTableStatement {
2186            name,
2187            columns,
2188            if_not_exists,
2189            foreign_keys,
2190            table_constraints,
2191        }))
2192    }
2193
2194    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
2195    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
2196    /// sure (otherwise a column literally named `primary` would
2197    /// be mistaken).
2198    fn peek_table_level_pk_start(&self) -> bool {
2199        let cur = self.peek();
2200        let nxt = self.tokens.get(self.pos + 1);
2201        let nxt2 = self.tokens.get(self.pos + 2);
2202        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
2203        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
2204        let is_lparen = matches!(nxt2, Some(Token::LParen));
2205        is_primary && is_key && is_lparen
2206    }
2207
2208    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
2209    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
2210    /// (mailrs round-5 G10).
2211    fn peek_table_level_unique_start(&self) -> bool {
2212        let cur = self.peek();
2213        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
2214        if !is_unique {
2215            return false;
2216        }
2217        let n1 = self.tokens.get(self.pos + 1);
2218        // Plain `UNIQUE (…)`.
2219        if matches!(n1, Some(Token::LParen)) {
2220            return true;
2221        }
2222        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
2223        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
2224        if !is_nulls {
2225            return false;
2226        }
2227        let n2 = self.tokens.get(self.pos + 2);
2228        let n3 = self.tokens.get(self.pos + 3);
2229        let n4 = self.tokens.get(self.pos + 4);
2230        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
2231        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
2232            return true;
2233        }
2234        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
2235        if matches!(n2, Some(Token::Not))
2236            && matches!(n3, Some(Token::Distinct))
2237            && matches!(n4, Some(Token::LParen))
2238        {
2239            return true;
2240        }
2241        false
2242    }
2243
2244    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2245        self.advance(); // PRIMARY
2246        self.advance(); // KEY
2247        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
2248        Ok(crate::ast::TableConstraint::PrimaryKey {
2249            name: None,
2250            columns,
2251        })
2252    }
2253
2254    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2255        self.advance(); // UNIQUE
2256        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
2257        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
2258        // is `NULLS DISTINCT` per the SQL standard.
2259        let mut nulls_not_distinct = false;
2260        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
2261            let n1 = self.tokens.get(self.pos + 1);
2262            let n2 = self.tokens.get(self.pos + 2);
2263            let is_not = matches!(n1, Some(Token::Not));
2264            let is_distinct = matches!(n2, Some(Token::Distinct));
2265            if is_not && is_distinct {
2266                self.advance(); // NULLS
2267                self.advance(); // NOT
2268                self.advance(); // DISTINCT
2269                nulls_not_distinct = true;
2270            } else if matches!(n1, Some(Token::Distinct)) {
2271                self.advance(); // NULLS
2272                self.advance(); // DISTINCT
2273            }
2274        }
2275        let columns = self.parse_paren_ident_list("UNIQUE")?;
2276        Ok(crate::ast::TableConstraint::Unique {
2277            name: None,
2278            columns,
2279            nulls_not_distinct,
2280        })
2281    }
2282
2283    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
2284    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
2285    /// expression.
2286    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
2287        self.advance(); // CHECK
2288        if !matches!(self.peek(), Token::LParen) {
2289            return Err(self.err(alloc::format!(
2290                "expected '(' after CHECK, got {:?}",
2291                self.peek()
2292            )));
2293        }
2294        self.advance();
2295        let expr = self.parse_expr(0)?;
2296        if !matches!(self.peek(), Token::RParen) {
2297            return Err(self.err(alloc::format!(
2298                "expected ')' to close CHECK predicate, got {:?}",
2299                self.peek()
2300            )));
2301        }
2302        self.advance();
2303        Ok(crate::ast::TableConstraint::Check { name: None, expr })
2304    }
2305
2306    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
2307    fn peek_table_level_check_start(&self) -> bool {
2308        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
2309    }
2310
2311    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
2312        if !matches!(self.peek(), Token::LParen) {
2313            return Err(self.err(alloc::format!(
2314                "expected '(' after {ctx}, got {:?}",
2315                self.peek()
2316            )));
2317        }
2318        self.advance();
2319        let mut out = Vec::new();
2320        loop {
2321            out.push(self.expect_ident_like()?);
2322            match self.peek() {
2323                Token::Comma => {
2324                    self.advance();
2325                }
2326                Token::RParen => {
2327                    self.advance();
2328                    break;
2329                }
2330                other => {
2331                    return Err(self.err(alloc::format!(
2332                        "expected ',' or ')' in {ctx} list, got {other:?}"
2333                    )));
2334                }
2335            }
2336        }
2337        if out.is_empty() {
2338            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
2339        }
2340        Ok(out)
2341    }
2342
2343    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
2344    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
2345    /// table-level FK; a column def never starts with either keyword
2346    /// (column names are not in this reserved set).
2347    fn peek_constraint_or_fk_start(&self) -> bool {
2348        let is_constraint_kw = matches!(
2349            self.peek(),
2350            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
2351        );
2352        let is_foreign_kw = matches!(
2353            self.peek(),
2354            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
2355        );
2356        is_constraint_kw || is_foreign_kw
2357    }
2358
2359    /// v7.6.0 — parse a table-level FK clause:
2360    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
2361    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
2362    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
2363        let mut name: Option<String> = None;
2364        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
2365            self.advance();
2366            name = Some(self.expect_ident_like()?);
2367        }
2368        // `FOREIGN`
2369        match self.advance() {
2370            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
2371            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
2372        }
2373        // `KEY`
2374        match self.advance() {
2375            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
2376            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
2377        }
2378        // `(col, col, ...)`
2379        if !matches!(self.peek(), Token::LParen) {
2380            return Err(self.err(format!(
2381                "expected '(' after FOREIGN KEY, got {:?}",
2382                self.peek()
2383            )));
2384        }
2385        self.advance();
2386        let mut columns = Vec::new();
2387        loop {
2388            columns.push(self.expect_ident_like()?);
2389            match self.peek() {
2390                Token::Comma => {
2391                    self.advance();
2392                }
2393                Token::RParen => {
2394                    self.advance();
2395                    break;
2396                }
2397                other => {
2398                    return Err(self.err(format!(
2399                        "expected ',' or ')' in FK column list, got {other:?}"
2400                    )));
2401                }
2402            }
2403        }
2404        if columns.is_empty() {
2405            return Err(self.err("FOREIGN KEY requires at least one column".into()));
2406        }
2407        let (parent_table, parent_columns, on_delete, on_update) =
2408            self.parse_references_tail(columns.len())?;
2409        Ok(ForeignKeyConstraint {
2410            name,
2411            columns,
2412            parent_table,
2413            parent_columns,
2414            on_delete,
2415            on_update,
2416        })
2417    }
2418
2419    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
2420    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
2421    /// the local column count, used to default the parent column
2422    /// list when omitted (SQL spec: parent's PK is implied).
2423    fn parse_references_tail(
2424        &mut self,
2425        expected_arity: usize,
2426    ) -> Result<(String, Vec<String>, FkAction, FkAction), ParseError> {
2427        match self.advance() {
2428            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
2429            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
2430        }
2431        let parent_table = self.expect_ident_like()?;
2432        let mut parent_columns: Vec<String> = Vec::new();
2433        if matches!(self.peek(), Token::LParen) {
2434            self.advance();
2435            loop {
2436                parent_columns.push(self.expect_ident_like()?);
2437                match self.peek() {
2438                    Token::Comma => {
2439                        self.advance();
2440                    }
2441                    Token::RParen => {
2442                        self.advance();
2443                        break;
2444                    }
2445                    other => {
2446                        return Err(self.err(format!(
2447                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
2448                        )));
2449                    }
2450                }
2451            }
2452        }
2453        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
2454            return Err(self.err(format!(
2455                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
2456                expected_arity,
2457                parent_columns.len()
2458            )));
2459        }
2460        // v7.6.7 — accept and reject `[NOT] DEFERRABLE [INITIALLY
2461        // {DEFERRED | IMMEDIATE}]` so existing PG dumps don't fail
2462        // at parse time. SPG's single-writer model has no deferred
2463        // constraint window, so we surface this as a clean
2464        // unsupported-feature error rather than a syntax error.
2465        loop {
2466            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
2467                return Err(self.err(
2468                    "DEFERRABLE constraints are not supported (SPG is single-writer; \
2469                     constraints are always evaluated immediately at commit)"
2470                        .into(),
2471                ));
2472            }
2473            if matches!(self.peek(), Token::Not) {
2474                let look = self.tokens.get(self.pos + 1);
2475                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
2476                    // NOT DEFERRABLE — accept as the SPG default
2477                    // and consume both tokens silently.
2478                    self.advance();
2479                    self.advance();
2480                    // Optional `INITIALLY IMMEDIATE` clause.
2481                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially"))
2482                    {
2483                        self.advance();
2484                        match self.advance() {
2485                            Token::Ident(s) if s.eq_ignore_ascii_case("immediate") => {}
2486                            other => {
2487                                return Err(self.err(format!(
2488                                    "expected IMMEDIATE after INITIALLY for NOT DEFERRABLE, \
2489                                     got {other:?}"
2490                                )));
2491                            }
2492                        }
2493                    }
2494                    continue;
2495                }
2496                break;
2497            }
2498            break;
2499        }
2500        // Optional `ON DELETE <action>` and `ON UPDATE <action>` in
2501        // either order, each at most once.
2502        let mut on_delete = FkAction::Restrict;
2503        let mut on_update = FkAction::Restrict;
2504        let mut seen_on_delete = false;
2505        let mut seen_on_update = false;
2506        loop {
2507            if !matches!(self.peek(), Token::On) {
2508                break;
2509            }
2510            self.advance();
2511            let which = self.advance();
2512            let action = self.parse_fk_action()?;
2513            match which {
2514                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
2515                    if seen_on_delete {
2516                        return Err(self.err("ON DELETE specified twice".into()));
2517                    }
2518                    seen_on_delete = true;
2519                    on_delete = action;
2520                }
2521                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
2522                    if seen_on_update {
2523                        return Err(self.err("ON UPDATE specified twice".into()));
2524                    }
2525                    seen_on_update = true;
2526                    on_update = action;
2527                }
2528                other => {
2529                    return Err(
2530                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
2531                    );
2532                }
2533            }
2534        }
2535        Ok((parent_table, parent_columns, on_delete, on_update))
2536    }
2537
2538    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
2539    /// NO ACTION`.
2540    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
2541        match self.advance() {
2542            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
2543            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
2544            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
2545                Token::Null => Ok(FkAction::SetNull),
2546                Token::Default => Ok(FkAction::SetDefault),
2547                other => Err(self.err(format!(
2548                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
2549                ))),
2550            },
2551            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
2552                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
2553                other => Err(self.err(format!(
2554                    "expected ACTION after NO in FK action, got {other:?}"
2555                ))),
2556            },
2557            other => Err(self.err(format!(
2558                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
2559            ))),
2560        }
2561    }
2562
2563    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
2564    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
2565    fn consume_if_not_exists(&mut self) -> bool {
2566        // `IF` arrives as a bare Ident (we don't reserve it because it
2567        // also appears mid-expression in PG, though we don't support
2568        // those forms yet).
2569        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
2570        if !looks_like_if {
2571            return false;
2572        }
2573        // Peek one ahead before committing: only consume IF when it's
2574        // actually `IF NOT EXISTS`.
2575        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
2576            return false;
2577        }
2578        if !matches!(
2579            self.tokens.get(self.pos + 2),
2580            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
2581        ) {
2582            return false;
2583        }
2584        self.advance(); // IF
2585        self.advance(); // NOT
2586        self.advance(); // EXISTS
2587        true
2588    }
2589
2590    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
2591    /// Consumes IF EXISTS as a pair; returns false otherwise
2592    /// without consuming any tokens.
2593    fn consume_if_exists(&mut self) -> bool {
2594        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
2595        if !looks_like_if {
2596            return false;
2597        }
2598        if !matches!(
2599            self.tokens.get(self.pos + 1),
2600            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
2601        ) {
2602            return false;
2603        }
2604        self.advance(); // IF
2605        self.advance(); // EXISTS
2606        true
2607    }
2608
2609    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
2610    /// qualifiers after an index column ref. ASC / DESC are
2611    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
2612    /// We accept and discard them since single-column BTree
2613    /// stores rows in natural key order today.
2614    fn consume_optional_index_column_qualifiers(&mut self) {
2615        loop {
2616            match self.peek() {
2617                Token::Asc | Token::Desc => {
2618                    self.advance();
2619                }
2620                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
2621                    let look = self.tokens.get(self.pos + 1);
2622                    if matches!(
2623                        look,
2624                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
2625                            || k.eq_ignore_ascii_case("last")
2626                    ) {
2627                        self.advance();
2628                        self.advance();
2629                    } else {
2630                        break;
2631                    }
2632                }
2633                _ => break,
2634            }
2635        }
2636    }
2637
2638    fn parse_create_index_stmt_after_create(
2639        &mut self,
2640        is_unique: bool,
2641    ) -> Result<Statement, ParseError> {
2642        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
2643        debug_assert!(matches!(self.peek(), Token::Index));
2644        self.advance();
2645        let if_not_exists = self.consume_if_not_exists();
2646        let name = self.expect_ident_like()?;
2647        if !matches!(self.peek(), Token::On) {
2648            return Err(self.err(format!(
2649                "expected ON after CREATE INDEX <name>, got {:?}",
2650                self.peek()
2651            )));
2652        }
2653        self.advance();
2654        let table = self.expect_ident_like()?;
2655        // Optional `USING <method>` — only recognised method in v2.0 is
2656        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
2657        // ident `using` (we don't promote it to a reserved keyword
2658        // because it isn't reserved anywhere else in our SQL surface).
2659        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
2660            self.advance();
2661            let m = self.expect_ident_like()?;
2662            match m.to_ascii_lowercase().as_str() {
2663                "hnsw" => IndexMethod::Hnsw,
2664                "btree" => IndexMethod::BTree,
2665                "brin" => IndexMethod::Brin,
2666                // v7.12.3 — real GIN inverted index over `tsvector`.
2667                // v7.9.26b's `USING gin` → BTree silent fallback is
2668                // gone; the engine validates that the indexed column
2669                // is `tsvector` at CREATE INDEX time.
2670                "gin" => IndexMethod::Gin,
2671                // v7.9.26b — PG `pg_dump` emits `USING gist` /
2672                // `USING spgist` / `USING hash` for their built-in
2673                // AMs that SPG doesn't have a matching
2674                // implementation for; degrade to BTree on the
2675                // leading column so the schema loads + the index
2676                // catalogue stays consistent. Operator pays the
2677                // planner cost only for the queries that would have
2678                // used the specialised AM.
2679                "gist" | "spgist" | "hash" => IndexMethod::BTree,
2680                // v7.11.3 — pgvector ships both `ivfflat` and
2681                // `hnsw`. Customers shouldn't have to choose
2682                // their on-disk index method based on what SPG
2683                // implements; accept `ivfflat` as a synonym for
2684                // `hnsw` so PG schemas using either method drop
2685                // in. The vector distance op (`<->` / `<#>` /
2686                // `<=>`) at query time still picks the metric.
2687                "ivfflat" => IndexMethod::Hnsw,
2688                other => {
2689                    return Err(self.err(alloc::format!(
2690                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
2691                    )));
2692                }
2693            }
2694        } else {
2695            IndexMethod::BTree
2696        };
2697        if !matches!(self.peek(), Token::LParen) {
2698            return Err(self.err(format!(
2699                "expected '(' before indexed column, got {:?}",
2700                self.peek()
2701            )));
2702        }
2703        self.advance();
2704        // v6.8.2 — accept either a bare column ident (legacy) or
2705        // an expression `fn(col, …)` for expression indexes.
2706        // Distinguish by peeking the token *after* the current
2707        // ident: `ident )` is the legacy column-only path;
2708        // anything else triggers the Pratt expression parser.
2709        // (`advance()` uses `mem::replace` to nil out the current
2710        // slot, so we can't save+rewind cleanly — peek-ahead via
2711        // direct index avoids the mutation.)
2712        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
2713            // Single column with `)` immediately after — fast path.
2714            // v7.9.29 — also: bare column followed by `,` (the
2715            // multi-column form `(a, b, c)`). Without this branch
2716            // the leading ident gets pulled into `parse_expr`
2717            // which then sets `expression = Some(Column(a))` and
2718            // breaks Display round-trip on the multi-column shape.
2719            Token::Ident(s) | Token::QuotedIdent(s)
2720                if matches!(
2721                    self.tokens.get(self.pos + 1),
2722                    Some(Token::RParen | Token::Comma)
2723                ) =>
2724            {
2725                self.advance();
2726                (s, None)
2727            }
2728            // v7.9.22 — single column followed by a pgvector
2729            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
2730            // SPG's HNSW currently picks its distance metric from
2731            // the query's operator (`<->` / `<#>` / `<=>`), so the
2732            // opclass is informational — accepted and discarded.
2733            // Recognised opclasses: vector_cosine_ops, vector_l2_ops,
2734            // vector_ip_ops, halfvec_*_ops, sq8_*_ops.
2735            Token::Ident(s) | Token::QuotedIdent(s)
2736                if matches!(
2737                    self.tokens.get(self.pos + 1),
2738                    Some(Token::Ident(op) | Token::QuotedIdent(op))
2739                        if is_vector_opclass_name(op)
2740                ) =>
2741            {
2742                self.advance(); // column name
2743                self.advance(); // opclass ident — drop
2744                (s, None)
2745            }
2746            Token::Ident(_) | Token::QuotedIdent(_) => {
2747                let key_expr = self.parse_expr(0)?;
2748                let primary = extract_first_column(&key_expr).ok_or_else(|| {
2749                    self.err("expression index key must reference at least one column".into())
2750                })?;
2751                (primary, Some(key_expr))
2752            }
2753            other => {
2754                return Err(self.err(format!(
2755                    "expected column ident or expression, got {other:?}"
2756                )));
2757            }
2758        };
2759        // v7.9.14 — accept extra comma-separated columns inside
2760        // the index key parens (`CREATE INDEX … (a, b, c)`).
2761        // mailrs F2. Each extra column may carry an optional
2762        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
2763        // — parsed and discarded; SPG doesn't honour direction
2764        // on a BTree index today (column ordering is intrinsic
2765        // to the storage). v7.10 will widen to genuine composite
2766        // index keys.
2767        let mut extra_columns: Vec<String> = Vec::new();
2768        // The leading column may also have ASC/DESC after it.
2769        self.consume_optional_index_column_qualifiers();
2770        while matches!(self.peek(), Token::Comma) {
2771            self.advance();
2772            let extra = self.expect_ident_like()?;
2773            self.consume_optional_index_column_qualifiers();
2774            extra_columns.push(extra);
2775        }
2776        if !matches!(self.peek(), Token::RParen) {
2777            return Err(self.err(format!(
2778                "expected ')' after indexed column / expression, got {:?}",
2779                self.peek()
2780            )));
2781        }
2782        self.advance();
2783        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
2784        // index-only-scan annotation. Bare ident (not a reserved
2785        // keyword) so we test by case-insensitive string match.
2786        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
2787        {
2788            self.advance();
2789            if !matches!(self.peek(), Token::LParen) {
2790                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
2791            }
2792            self.advance();
2793            let mut cols = Vec::new();
2794            loop {
2795                cols.push(self.expect_ident_like()?);
2796                match self.peek() {
2797                    Token::Comma => {
2798                        self.advance();
2799                    }
2800                    Token::RParen => {
2801                        self.advance();
2802                        break;
2803                    }
2804                    other => {
2805                        return Err(self.err(format!(
2806                            "expected ',' or ')' in INCLUDE list, got {other:?}"
2807                        )));
2808                    }
2809                }
2810            }
2811            cols
2812        } else {
2813            Vec::new()
2814        };
2815        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
2816        // storage parameters. pgvector emits `WITH (lists = N)` for
2817        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
2818        // SPG's HNSW picks its own parameters today (tunable via
2819        // env vars), so the WITH clause is informational and dropped.
2820        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
2821            self.advance();
2822            if !matches!(self.peek(), Token::LParen) {
2823                return Err(self.err(format!(
2824                    "expected '(' after WITH in CREATE INDEX, got {:?}",
2825                    self.peek()
2826                )));
2827            }
2828            self.advance();
2829            loop {
2830                if matches!(self.peek(), Token::RParen) {
2831                    self.advance();
2832                    break;
2833                }
2834                // Drain `key = value` or bare `key` tokens.
2835                let _ = self.advance(); // key
2836                if matches!(self.peek(), Token::Eq) {
2837                    self.advance();
2838                    let _ = self.advance(); // value (int / string / ident)
2839                }
2840                match self.peek() {
2841                    Token::Comma => {
2842                        self.advance();
2843                    }
2844                    Token::RParen => {
2845                        self.advance();
2846                        break;
2847                    }
2848                    other => {
2849                        return Err(self.err(format!(
2850                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
2851                        )));
2852                    }
2853                }
2854            }
2855        }
2856        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
2857        let partial_predicate = if matches!(self.peek(), Token::Where) {
2858            self.advance();
2859            Some(self.parse_expr(0)?)
2860        } else {
2861            None
2862        };
2863        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
2864        // sense: uniqueness over an ANN structure has no clean
2865        // semantics. Reject early. (BRIN UNIQUE is similarly
2866        // meaningless — block both.)
2867        if is_unique && !matches!(method, IndexMethod::BTree) {
2868            return Err(self.err(alloc::format!(
2869                "UNIQUE is only supported on BTree indexes, got USING {:?}",
2870                method
2871            )));
2872        }
2873        Ok(Statement::CreateIndex(CreateIndexStatement {
2874            name,
2875            table,
2876            column,
2877            method,
2878            if_not_exists,
2879            included_columns,
2880            partial_predicate,
2881            extra_columns: extra_columns.clone(),
2882            expression,
2883            is_unique,
2884        }))
2885    }
2886
2887    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
2888    /// column-level `REFERENCES ...` clause. The trailing FK is
2889    /// normalised into table-level shape (single-element columns +
2890    /// parent_columns) so the engine sees one uniform constraint list.
2891    fn parse_column_def_with_fk(
2892        &mut self,
2893    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
2894        let col = self.parse_column_def()?;
2895        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
2896        let inline_references = matches!(
2897            self.peek(),
2898            Token::Ident(s) if s.eq_ignore_ascii_case("references")
2899        );
2900        if !inline_references {
2901            return Ok((col, None));
2902        }
2903        let (parent_table, parent_columns, on_delete, on_update) = self.parse_references_tail(1)?;
2904        let fk = ForeignKeyConstraint {
2905            name: None,
2906            columns: vec![col.name.clone()],
2907            parent_table,
2908            parent_columns,
2909            on_delete,
2910            on_update,
2911        };
2912        Ok((col, Some(fk)))
2913    }
2914
2915    /// v7.13.0 — parse a column type (consuming the type ident and
2916    /// any trailing parameters / `[]`), without surrounding column
2917    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
2918    /// Returns the resolved `ColumnTypeName` plus implied
2919    /// `(auto_increment, not_null)` flags from PG SERIAL family
2920    /// shorthands — callers that don't expect those (ALTER COLUMN
2921    /// TYPE) can discard them.
2922    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
2923        let (ty, _, _) = self.parse_type_with_implied_flags()?;
2924        Ok(ty)
2925    }
2926
2927    fn parse_type_with_implied_flags(
2928        &mut self,
2929    ) -> Result<(ColumnTypeName, bool, bool), ParseError> {
2930        let ty_ident = match self.advance() {
2931            Token::Ident(s) => s,
2932            other => {
2933                return Err(ParseError {
2934                    message: format!("expected column type, got {other:?}"),
2935                    token_pos: self.pos.saturating_sub(1),
2936                });
2937            }
2938        };
2939        let mut implied_auto_increment = false;
2940        let mut implied_not_null = false;
2941        let mut ty = match ty_ident.as_str() {
2942            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
2943            "smallserial" | "serial2" => {
2944                implied_auto_increment = true;
2945                implied_not_null = true;
2946                ColumnTypeName::SmallInt
2947            }
2948            "serial" | "serial4" => {
2949                implied_auto_increment = true;
2950                implied_not_null = true;
2951                ColumnTypeName::Int
2952            }
2953            "bigserial" | "serial8" => {
2954                implied_auto_increment = true;
2955                implied_not_null = true;
2956                ColumnTypeName::BigInt
2957            }
2958            // MySQL flavours we accept by aliasing to the closest SPG
2959            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
2960            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
2961            // 24-bit) → INT. UNSIGNED modifiers are consumed below
2962            // without semantic effect.
2963            "smallint" | "tinyint" => ColumnTypeName::SmallInt,
2964            // INTEGER is MySQL's spelling for INT; MEDIUMINT widens up.
2965            "int" | "integer" | "mediumint" => ColumnTypeName::Int,
2966            "bigint" => ColumnTypeName::BigInt,
2967            // DOUBLE / REAL are 64-bit IEEE — same as our FLOAT.
2968            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
2969            // (mailrs round-5 G6). Consume the optional `PRECISION`
2970            // tail when the type keyword was `double` / `DOUBLE`.
2971            "float" | "double" | "real" => {
2972                if ty_ident.eq_ignore_ascii_case("double")
2973                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
2974                {
2975                    self.advance();
2976                }
2977                ColumnTypeName::Float
2978            }
2979            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
2980            "float4" | "float8" => ColumnTypeName::Float,
2981            "text" => ColumnTypeName::Text,
2982            "bool" | "boolean" => ColumnTypeName::Bool,
2983            "varchar" => ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?),
2984            "char" => ColumnTypeName::Char(self.parse_paren_size("CHAR")?),
2985            "vector" => {
2986                let dim = self.parse_paren_size("VECTOR")?;
2987                let encoding = self.parse_optional_vector_encoding()?;
2988                ColumnTypeName::Vector { dim, encoding }
2989            }
2990            "numeric" => {
2991                let (precision, scale) = self.parse_optional_numeric_params()?;
2992                ColumnTypeName::Numeric(precision, scale)
2993            }
2994            "date" => ColumnTypeName::Date,
2995            // MySQL's `DATETIME` is the same domain as standard
2996            // `TIMESTAMP` — accept both spellings.
2997            "timestamp" | "datetime" => ColumnTypeName::Timestamp,
2998            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
2999            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
3000            // only PG-wire OID differs.
3001            "timestamptz" => ColumnTypeName::Timestamptz,
3002            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
3003            // validation. We accept the JSONB spelling too because
3004            // most PG clients default to it; SPG doesn't distinguish
3005            // the two (no path-operator perf advantage to model).
3006            "json" => ColumnTypeName::Json,
3007            "jsonb" => ColumnTypeName::Jsonb,
3008            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
3009            // surface here. Same storage shape; mapping happens at
3010            // the engine side via the ColumnTypeName → DataType
3011            // resolver. Literal forms are handled at coerce_value
3012            // time so the lexer stays untouched.
3013            "bytea" | "bytes" => ColumnTypeName::Bytes,
3014            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
3015            // The actual `to_tsvector` / `@@` / `ts_rank` surface
3016            // arrives in v7.12.1+; the type itself loads here so
3017            // mailrs's `scripts/init-schema.sql` runs unmodified.
3018            "tsvector" => ColumnTypeName::TsVector,
3019            "tsquery" => ColumnTypeName::TsQuery,
3020            other => {
3021                return Err(ParseError {
3022                    message: format!("unsupported column type {other:?}"),
3023                    token_pos: self.pos.saturating_sub(1),
3024                });
3025            }
3026        };
3027        // MySQL's `UNSIGNED` modifier sits right after the type
3028        // keyword. SPG doesn't carry a separate unsigned variant —
3029        // accepting the keyword keeps existing schemas compatible
3030        // without changing semantics. Drop it silently.
3031        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned")) {
3032            self.advance();
3033        }
3034        // v7.10.10 — postfix `[]` widens TEXT → TEXT[]. PG accepts
3035        // `TYPE[]` after any base type; v7.10 only models TEXT[]
3036        // so we reject other base types here. mailrs uses TEXT[]
3037        // for labels / addresses / message-on-thread.
3038        if matches!(self.peek(), Token::LBracket) {
3039            self.advance();
3040            if !matches!(self.peek(), Token::RBracket) {
3041                return Err(self.err(alloc::format!(
3042                    "TEXT[] takes no dimension; got {:?}",
3043                    self.peek()
3044                )));
3045            }
3046            self.advance();
3047            // v7.11.13 — widened to INT[] and BIGINT[] in addition
3048            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
3049            // still error here.
3050            ty = match ty {
3051                ColumnTypeName::Text => ColumnTypeName::TextArray,
3052                ColumnTypeName::Int => ColumnTypeName::IntArray,
3053                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
3054                other => {
3055                    return Err(self.err(alloc::format!(
3056                        "v7.11 supports TEXT[] / INT[] / BIGINT[] only; got {other:?}[]"
3057                    )));
3058                }
3059            };
3060        }
3061        Ok((ty, implied_auto_increment, implied_not_null))
3062    }
3063
3064    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
3065        let name = self.expect_ident_like()?;
3066        let (ty, implied_auto_increment, implied_not_null) =
3067            self.parse_type_with_implied_flags()?;
3068        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
3069        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
3070        // each at most once.
3071        let mut default: Option<Expr> = None;
3072        let mut nullable = !implied_not_null;
3073        let mut nullability_seen = implied_not_null;
3074        let mut auto_increment = implied_auto_increment;
3075        let mut is_primary_key = false;
3076        let mut is_unique = false;
3077        let mut check: Option<Expr> = None;
3078        loop {
3079            if matches!(self.peek(), Token::Default) {
3080                if default.is_some() {
3081                    return Err(self.err("DEFAULT specified twice".into()));
3082                }
3083                self.advance();
3084                default = Some(self.parse_expr(0)?);
3085                continue;
3086            }
3087            if matches!(self.peek(), Token::Not) {
3088                if nullability_seen {
3089                    return Err(self.err("NOT NULL specified twice".into()));
3090                }
3091                self.advance();
3092                if !matches!(self.peek(), Token::Null) {
3093                    return Err(self.err(format!(
3094                        "expected NULL after NOT in column def, got {:?}",
3095                        self.peek()
3096                    )));
3097                }
3098                self.advance();
3099                nullable = false;
3100                nullability_seen = true;
3101                continue;
3102            }
3103            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
3104            // arrives as a bare Ident. Match either, case-insensitive.
3105            if let Token::Ident(s) = self.peek()
3106                && (s.eq_ignore_ascii_case("auto_increment")
3107                    || s.eq_ignore_ascii_case("autoincrement"))
3108            {
3109                if auto_increment {
3110                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
3111                }
3112                self.advance();
3113                auto_increment = true;
3114                continue;
3115            }
3116            // v7.9.13 — inline `PRIMARY KEY` column constraint
3117            // (mailrs F1). Implies `NOT NULL`. The engine creates
3118            // a BTree index for the PK column at CREATE TABLE time
3119            // so FK parent-side index lookups resolve.
3120            if let Token::Ident(s) = self.peek()
3121                && s.eq_ignore_ascii_case("primary")
3122            {
3123                if is_primary_key {
3124                    return Err(self.err("PRIMARY KEY specified twice".into()));
3125                }
3126                // Peek-ahead for the required `KEY` token.
3127                let next = self.tokens.get(self.pos + 1);
3128                let next_is_key = matches!(
3129                    next,
3130                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
3131                );
3132                if !next_is_key {
3133                    return Err(self.err(format!(
3134                        "expected KEY after PRIMARY in column def, got {:?}",
3135                        next
3136                    )));
3137                }
3138                self.advance(); // PRIMARY
3139                self.advance(); // KEY
3140                is_primary_key = true;
3141                if nullability_seen && nullable {
3142                    return Err(self.err(
3143                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
3144                    ));
3145                }
3146                nullable = false;
3147                nullability_seen = true;
3148                continue;
3149            }
3150            // v7.13.0 — inline `UNIQUE` column constraint
3151            // (mailrs round-5 G2). Fold into a single-column
3152            // table-level UNIQUE at CREATE TABLE post-process time.
3153            if let Token::Ident(s) = self.peek()
3154                && s.eq_ignore_ascii_case("unique")
3155            {
3156                if is_unique {
3157                    return Err(self.err("UNIQUE specified twice".into()));
3158                }
3159                self.advance();
3160                is_unique = true;
3161                continue;
3162            }
3163            // v7.13.0 — inline `CHECK (<expr>)` column constraint
3164            // (mailrs round-5 G3). PG semantics: column-level
3165            // CHECK is equivalent to a table-level CHECK. Multiple
3166            // inline CHECKs on the same column AND together.
3167            if let Token::Ident(s) = self.peek()
3168                && s.eq_ignore_ascii_case("check")
3169            {
3170                self.advance();
3171                if !matches!(self.peek(), Token::LParen) {
3172                    return Err(self.err(alloc::format!(
3173                        "expected '(' after CHECK in column def, got {:?}",
3174                        self.peek()
3175                    )));
3176                }
3177                self.advance();
3178                let pred = self.parse_expr(0)?;
3179                if !matches!(self.peek(), Token::RParen) {
3180                    return Err(self.err(alloc::format!(
3181                        "expected ')' to close CHECK predicate, got {:?}",
3182                        self.peek()
3183                    )));
3184                }
3185                self.advance();
3186                check = Some(match check.take() {
3187                    Some(prev) => Expr::Binary {
3188                        op: BinOp::And,
3189                        lhs: Box::new(prev),
3190                        rhs: Box::new(pred),
3191                    },
3192                    None => pred,
3193                });
3194                continue;
3195            }
3196            break;
3197        }
3198        Ok(ColumnDef {
3199            name,
3200            ty,
3201            nullable,
3202            default,
3203            auto_increment,
3204            is_primary_key,
3205            is_unique,
3206            check,
3207        })
3208    }
3209
3210    /// `NUMERIC` may appear without parameters, with one (precision
3211    /// only, scale=0), or with both. Returns `(precision, scale)` with
3212    /// 0 = unspecified for the bare form.
3213    fn parse_optional_numeric_params(&mut self) -> Result<(u8, u8), ParseError> {
3214        if !matches!(self.peek(), Token::LParen) {
3215            // Bare `NUMERIC` — PG treats this as "unlimited precision";
3216            // we surface it as precision=0 to mean "unconstrained" so
3217            // the engine doesn't need a separate variant.
3218            return Ok((0, 0));
3219        }
3220        self.advance();
3221        let precision = match self.advance() {
3222            Token::Integer(n) if (1..=38).contains(&n) => u8::try_from(n).expect("range-checked"),
3223            other => {
3224                return Err(ParseError {
3225                    message: format!(
3226                        "NUMERIC precision must be an integer in 1..=38, got {other:?}"
3227                    ),
3228                    token_pos: self.pos.saturating_sub(1),
3229                });
3230            }
3231        };
3232        let scale = if matches!(self.peek(), Token::Comma) {
3233            self.advance();
3234            match self.advance() {
3235                Token::Integer(n) if (0..=i64::from(precision)).contains(&n) => {
3236                    u8::try_from(n).expect("range-checked")
3237                }
3238                other => {
3239                    return Err(ParseError {
3240                        message: format!(
3241                            "NUMERIC scale must be a non-negative integer ≤ precision, got {other:?}"
3242                        ),
3243                        token_pos: self.pos.saturating_sub(1),
3244                    });
3245                }
3246            }
3247        } else {
3248            0
3249        };
3250        if !matches!(self.peek(), Token::RParen) {
3251            return Err(self.err(format!(
3252                "expected ')' to close NUMERIC params, got {:?}",
3253                self.peek()
3254            )));
3255        }
3256        self.advance();
3257        Ok((precision, scale))
3258    }
3259
3260    /// Parse `(N)` where `N` is a positive integer literal — used by the
3261    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
3262    /// for the error message.
3263    /// v6.0.1: parse the optional `USING <encoding>` clause that
3264    /// follows `VECTOR(N)` in a column definition. Missing clause
3265    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
3266    /// ident → `ParseError` listing the encodings recognised today.
3267    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
3268        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
3269            return Ok(VecEncoding::F32);
3270        }
3271        self.advance();
3272        let enc_ident = match self.advance() {
3273            Token::Ident(s) => s,
3274            other => {
3275                return Err(self.err(format!(
3276                    "expected vector encoding after USING, got {other:?}"
3277                )));
3278            }
3279        };
3280        match enc_ident.to_ascii_lowercase().as_str() {
3281            "sq8" => Ok(VecEncoding::Sq8),
3282            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
3283            // binary16 per-element storage.
3284            "half" => Ok(VecEncoding::F16),
3285            other => Err(self.err(format!(
3286                "unknown vector encoding {other:?}; supported: SQ8, HALF"
3287            ))),
3288        }
3289    }
3290
3291    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
3292        if !matches!(self.peek(), Token::LParen) {
3293            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
3294        }
3295        self.advance();
3296        let n = match self.advance() {
3297            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
3298                message: format!("{label} size too large: {n}"),
3299                token_pos: self.pos.saturating_sub(1),
3300            })?,
3301            other => {
3302                return Err(ParseError {
3303                    message: format!("expected positive integer {label} size, got {other:?}"),
3304                    token_pos: self.pos.saturating_sub(1),
3305                });
3306            }
3307        };
3308        if !matches!(self.peek(), Token::RParen) {
3309            return Err(self.err(format!(
3310                "expected ')' after {label} size, got {:?}",
3311                self.peek()
3312            )));
3313        }
3314        self.advance();
3315        Ok(n)
3316    }
3317
3318    fn parse_insert_stmt(&mut self) -> Result<Statement, ParseError> {
3319        debug_assert!(matches!(self.peek(), Token::Insert));
3320        self.advance();
3321        if !matches!(self.peek(), Token::Into) {
3322            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
3323        }
3324        self.advance();
3325        let table = self.expect_ident_like()?;
3326        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
3327        let columns = if matches!(self.peek(), Token::LParen) {
3328            self.advance();
3329            let mut names = Vec::new();
3330            loop {
3331                names.push(self.expect_ident_like()?);
3332                match self.peek() {
3333                    Token::Comma => {
3334                        self.advance();
3335                    }
3336                    Token::RParen => {
3337                        self.advance();
3338                        break;
3339                    }
3340                    other => {
3341                        return Err(self.err(format!(
3342                            "expected ',' or ')' in INSERT column list, got {other:?}"
3343                        )));
3344                    }
3345                }
3346            }
3347            Some(names)
3348        } else {
3349            None
3350        };
3351        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
3352        // round-5 G4). Dispatch on VALUES vs SELECT.
3353        if matches!(self.peek(), Token::Select) {
3354            let select_stmt = match self.parse_select_stmt()? {
3355                Statement::Select(s) => s,
3356                other => {
3357                    return Err(self.err(alloc::format!(
3358                        "expected SELECT after INSERT INTO ... target, got {other:?}"
3359                    )));
3360                }
3361            };
3362            let on_conflict = self.parse_optional_on_conflict()?;
3363            let returning = self.parse_optional_returning()?;
3364            return Ok(Statement::Insert(InsertStatement {
3365                table,
3366                columns,
3367                rows: Vec::new(),
3368                select_source: Some(Box::new(select_stmt)),
3369                on_conflict,
3370                returning,
3371            }));
3372        }
3373        if !matches!(self.peek(), Token::Values) {
3374            return Err(self.err(format!(
3375                "expected VALUES or SELECT after table name, got {:?}",
3376                self.peek()
3377            )));
3378        }
3379        self.advance();
3380        if !matches!(self.peek(), Token::LParen) {
3381            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
3382        }
3383        let mut rows = Vec::new();
3384        loop {
3385            // Each iteration consumes one `(expr, expr, …)` tuple.
3386            if !matches!(self.peek(), Token::LParen) {
3387                return Err(self.err(format!(
3388                    "expected '(' for next VALUES tuple, got {:?}",
3389                    self.peek()
3390                )));
3391            }
3392            self.advance();
3393            let mut tuple = Vec::new();
3394            loop {
3395                tuple.push(self.parse_expr(0)?);
3396                match self.peek() {
3397                    Token::Comma => {
3398                        self.advance();
3399                    }
3400                    Token::RParen => {
3401                        self.advance();
3402                        break;
3403                    }
3404                    other => {
3405                        return Err(self.err(format!(
3406                            "expected ',' or ')' in VALUES tuple, got {other:?}"
3407                        )));
3408                    }
3409                }
3410            }
3411            if tuple.is_empty() {
3412                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
3413            }
3414            rows.push(tuple);
3415            // Continue with comma-separated tuples.
3416            if matches!(self.peek(), Token::Comma) {
3417                self.advance();
3418            } else {
3419                break;
3420            }
3421        }
3422        let on_conflict = self.parse_optional_on_conflict()?;
3423        let returning = self.parse_optional_returning()?;
3424        Ok(Statement::Insert(InsertStatement {
3425            table,
3426            columns,
3427            rows,
3428            select_source: None,
3429            on_conflict,
3430            returning,
3431        }))
3432    }
3433
3434    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
3435    /// clause sitting between the INSERT body and the trailing
3436    /// RETURNING. All keywords come in as bare idents; `ON` is
3437    /// a reserved Token though.
3438    fn parse_optional_on_conflict(
3439        &mut self,
3440    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
3441        if !matches!(self.peek(), Token::On) {
3442            return Ok(None);
3443        }
3444        // Peek further: we want exactly "ON CONFLICT ...". If the
3445        // next ident isn't "conflict", let some other parser handle.
3446        let next_is_conflict = matches!(
3447            self.tokens.get(self.pos + 1),
3448            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
3449        );
3450        if !next_is_conflict {
3451            return Ok(None);
3452        }
3453        self.advance(); // ON
3454        self.advance(); // CONFLICT
3455        // Optional `(col [, col]*)` target list.
3456        let mut target_columns: Vec<String> = Vec::new();
3457        if matches!(self.peek(), Token::LParen) {
3458            self.advance();
3459            loop {
3460                target_columns.push(self.expect_ident_like()?);
3461                match self.peek() {
3462                    Token::Comma => {
3463                        self.advance();
3464                    }
3465                    Token::RParen => {
3466                        self.advance();
3467                        break;
3468                    }
3469                    other => {
3470                        return Err(self.err(alloc::format!(
3471                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
3472                        )));
3473                    }
3474                }
3475            }
3476        }
3477        // Required `DO`.
3478        match self.advance() {
3479            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
3480            other => {
3481                return Err(self.err(alloc::format!(
3482                    "expected DO after ON CONFLICT [(…)], got {other:?}"
3483                )));
3484            }
3485        }
3486        // Action: NOTHING | UPDATE SET …
3487        let action = match self.advance() {
3488            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
3489                crate::ast::OnConflictAction::Nothing
3490            }
3491            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3492                self.parse_on_conflict_update_action()?
3493            }
3494            other => {
3495                return Err(self.err(alloc::format!(
3496                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
3497                )));
3498            }
3499        };
3500        Ok(Some(crate::ast::OnConflictClause {
3501            target_columns,
3502            action,
3503        }))
3504    }
3505
3506    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
3507    /// `SET col = expr [, …] [WHERE cond]`. Caller already
3508    /// consumed `UPDATE`.
3509    fn parse_on_conflict_update_action(
3510        &mut self,
3511    ) -> Result<crate::ast::OnConflictAction, ParseError> {
3512        // `SET`
3513        match self.advance() {
3514            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
3515            other => {
3516                return Err(self.err(alloc::format!(
3517                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
3518                )));
3519            }
3520        }
3521        let mut assignments: Vec<(String, Expr)> = Vec::new();
3522        loop {
3523            let col = self.expect_ident_like()?;
3524            if !matches!(self.peek(), Token::Eq) {
3525                return Err(self.err(alloc::format!(
3526                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
3527                    self.peek()
3528                )));
3529            }
3530            self.advance();
3531            let value = self.parse_expr(0)?;
3532            assignments.push((col, value));
3533            if matches!(self.peek(), Token::Comma) {
3534                self.advance();
3535                continue;
3536            }
3537            break;
3538        }
3539        let where_ = if matches!(self.peek(), Token::Where) {
3540            self.advance();
3541            Some(self.parse_expr(0)?)
3542        } else {
3543            None
3544        };
3545        Ok(crate::ast::OnConflictAction::Update {
3546            assignments,
3547            where_,
3548        })
3549    }
3550
3551    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
3552        let mut items = Vec::new();
3553        loop {
3554            items.push(self.parse_select_item()?);
3555            if matches!(self.peek(), Token::Comma) {
3556                self.advance();
3557            } else {
3558                break;
3559            }
3560        }
3561        Ok(items)
3562    }
3563
3564    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
3565        if matches!(self.peek(), Token::Star) {
3566            self.advance();
3567            return Ok(SelectItem::Wildcard);
3568        }
3569        let expr = self.parse_expr(0)?;
3570        let alias = self.parse_optional_alias();
3571        Ok(SelectItem::Expr { expr, alias })
3572    }
3573
3574    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
3575        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
3576        // source. Detect at the head before the bare-ident fallback;
3577        // unnest is not a reserved token.
3578        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
3579            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
3580        {
3581            self.advance(); // unnest
3582            self.advance(); // (
3583            let expr = self.parse_expr(0)?;
3584            if !matches!(self.peek(), Token::RParen) {
3585                return Err(self.err(alloc::format!(
3586                    "expected ')' after unnest() argument, got {:?}",
3587                    self.peek()
3588                )));
3589            }
3590            self.advance();
3591            let alias_ident = self.parse_optional_alias();
3592            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
3593            return Ok(TableRef {
3594                name,
3595                alias: alias_ident,
3596                as_of_segment: None,
3597                unnest_expr: Some(Box::new(expr)),
3598            });
3599        }
3600        let name = self.expect_ident_like()?;
3601        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
3602        // time-travel clause. Parse BEFORE the alias so the
3603        // alias can still ride at the tail (`tbl AS OF SEGMENT
3604        // '5' alias`). `AS` is a reserved keyword token, while
3605        // `OF` and `SEGMENT` are bare idents.
3606        let as_of_segment = if matches!(self.peek(), Token::As)
3607            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
3608        {
3609            self.advance(); // AS
3610            self.advance(); // OF
3611            let kw = match self.peek().clone() {
3612                Token::Ident(s) | Token::QuotedIdent(s) => s,
3613                other => {
3614                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
3615                }
3616            };
3617            if !kw.eq_ignore_ascii_case("segment") {
3618                return Err(self.err(format!(
3619                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
3620                )));
3621            }
3622            self.advance();
3623            // Segment id literal — accept either a string or
3624            // integer for operator ergonomics.
3625            let id = match self.advance() {
3626                Token::String(s) => s
3627                    .parse::<u32>()
3628                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
3629                Token::Integer(n) => u32::try_from(n)
3630                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
3631                other => {
3632                    return Err(self.err(format!(
3633                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
3634                    )));
3635                }
3636            };
3637            Some(id)
3638        } else {
3639            None
3640        };
3641        let alias = self.parse_optional_alias();
3642        Ok(TableRef {
3643            name,
3644            alias,
3645            as_of_segment,
3646            unnest_expr: None,
3647        })
3648    }
3649
3650    /// FROM-clause: a primary table reference plus zero-or-more joined
3651    /// peers expressed via either `, <table>` (cross-product, no ON) or
3652    /// `[INNER|LEFT [OUTER]|CROSS] JOIN <table> [ON expr]`. v1.10 keeps
3653    /// the join list flat (left-associative nested-loop semantics).
3654    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
3655        let primary = self.parse_table_ref()?;
3656        let mut joins = Vec::new();
3657        loop {
3658            // `, <table>` — cross-product with no ON.
3659            if matches!(self.peek(), Token::Comma) {
3660                self.advance();
3661                let table = self.parse_table_ref()?;
3662                joins.push(FromJoin {
3663                    kind: JoinKind::Cross,
3664                    table,
3665                    on: None,
3666                });
3667                continue;
3668            }
3669            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
3670            // CROSS JOIN, and bare JOIN (defaults to INNER).
3671            let kind =
3672                match self.peek() {
3673                    Token::Inner => {
3674                        self.advance();
3675                        if !matches!(self.peek(), Token::Join) {
3676                            return Err(self
3677                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
3678                        }
3679                        self.advance();
3680                        JoinKind::Inner
3681                    }
3682                    Token::Left => {
3683                        self.advance();
3684                        if matches!(self.peek(), Token::Outer) {
3685                            self.advance();
3686                        }
3687                        if !matches!(self.peek(), Token::Join) {
3688                            return Err(self.err(format!(
3689                                "expected JOIN after LEFT [OUTER], got {:?}",
3690                                self.peek()
3691                            )));
3692                        }
3693                        self.advance();
3694                        JoinKind::Left
3695                    }
3696                    Token::Cross => {
3697                        self.advance();
3698                        if !matches!(self.peek(), Token::Join) {
3699                            return Err(self
3700                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
3701                        }
3702                        self.advance();
3703                        JoinKind::Cross
3704                    }
3705                    Token::Join => {
3706                        self.advance();
3707                        JoinKind::Inner
3708                    }
3709                    _ => break,
3710                };
3711            let table = self.parse_table_ref()?;
3712            let on = if matches!(self.peek(), Token::On) {
3713                self.advance();
3714                Some(self.parse_expr(0)?)
3715            } else if kind == JoinKind::Cross {
3716                None
3717            } else {
3718                return Err(self.err(format!(
3719                    "expected ON after {:?} JOIN, got {:?}",
3720                    kind,
3721                    self.peek()
3722                )));
3723            };
3724            joins.push(FromJoin { kind, table, on });
3725        }
3726        Ok(FromClause { primary, joins })
3727    }
3728
3729    /// Optional alias after an expression or table:
3730    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
3731    /// accepted (PG-style implicit alias). Returns `None` if the next token
3732    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
3733    fn parse_optional_alias(&mut self) -> Option<String> {
3734        if matches!(self.peek(), Token::As) {
3735            self.advance();
3736            // After AS, the next token MUST be an identifier-like — if not,
3737            // we still return None and let the caller surface the error on the
3738            // next expectation. v0.2 keeps the alias path forgiving; the
3739            // corpus tests don't exercise the malformed case.
3740            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
3741                return self.expect_ident_like().ok();
3742            }
3743            return None;
3744        }
3745        if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
3746            return self.expect_ident_like().ok();
3747        }
3748        None
3749    }
3750
3751    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
3752    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
3753        let mut lhs = self.parse_unary()?;
3754        while let Some((op, prec)) = binop_from(self.peek()) {
3755            if prec < min_prec {
3756                break;
3757            }
3758            self.advance();
3759            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
3760            // ANY is a bare ident; ALL is a reserved Token. Both
3761            // require an immediate `(` to disambiguate from
3762            // identifier columns named `any` / `all`.
3763            let any_kind = match self.peek() {
3764                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
3765                    Some(false)
3766                }
3767                Token::Ident(s) | Token::QuotedIdent(s)
3768                    if (s.eq_ignore_ascii_case("any") || s.eq_ignore_ascii_case("all"))
3769                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
3770                {
3771                    Some(s.eq_ignore_ascii_case("any"))
3772                }
3773                _ => None,
3774            };
3775            if let Some(is_any) = any_kind {
3776                self.advance(); // ident
3777                self.advance(); // (
3778                let arr = self.parse_expr(0)?;
3779                if !matches!(self.peek(), Token::RParen) {
3780                    return Err(self.err(alloc::format!(
3781                        "expected ')' after ANY/ALL argument, got {:?}",
3782                        self.peek()
3783                    )));
3784                }
3785                self.advance();
3786                lhs = Expr::AnyAll {
3787                    expr: Box::new(lhs),
3788                    op,
3789                    array: Box::new(arr),
3790                    is_any,
3791                };
3792                continue;
3793            }
3794            let rhs = self.parse_expr(prec + 1)?;
3795            lhs = Expr::Binary {
3796                lhs: Box::new(lhs),
3797                op,
3798                rhs: Box::new(rhs),
3799            };
3800        }
3801        Ok(lhs)
3802    }
3803
3804    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
3805        match self.peek() {
3806            Token::Not => {
3807                self.advance();
3808                // NOT sits between AND (2) and comparisons (4) — bind everything
3809                // ≥3, which leaves AND/OR outside.
3810                let e = self.parse_expr(3)?;
3811                Ok(Expr::Unary {
3812                    op: UnOp::Not,
3813                    expr: Box::new(e),
3814                })
3815            }
3816            Token::Minus => {
3817                self.advance();
3818                // Unary minus binds tighter than `*`/`/` (now at prec 7 after
3819                // `<->` slotted into 5 and arithmetic shifted up).
3820                let e = self.parse_expr(8)?;
3821                Ok(Expr::Unary {
3822                    op: UnOp::Neg,
3823                    expr: Box::new(e),
3824                })
3825            }
3826            _ => self.parse_atom(),
3827        }
3828    }
3829
3830    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
3831        let tok_pos = self.pos;
3832        match self.advance() {
3833            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
3834            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
3835            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
3836            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
3837            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
3838            Token::Null => Ok(Expr::Literal(Literal::Null)),
3839            // v6.1.1 — `$N` placeholder. The actual Value lookup
3840            // happens in the engine eval path against the prepared-
3841            // statement bind buffer.
3842            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
3843            Token::LParen => {
3844                // v4.10: `(SELECT ...)` in expression position is a
3845                // scalar subquery; otherwise it's a parenthesised
3846                // expression. Peek for SELECT keyword to dispatch.
3847                if matches!(self.peek(), Token::Select) {
3848                    let inner = self.parse_select_stmt()?;
3849                    match self.advance() {
3850                        Token::RParen => {
3851                            let Statement::Select(s) = inner else {
3852                                unreachable!("parse_select_stmt returns Select")
3853                            };
3854                            Ok(Expr::ScalarSubquery(Box::new(s)))
3855                        }
3856                        other => Err(ParseError {
3857                            message: format!("expected ')' after scalar subquery, got {other:?}"),
3858                            token_pos: self.pos.saturating_sub(1),
3859                        }),
3860                    }
3861                } else {
3862                    let e = self.parse_expr(0)?;
3863                    match self.advance() {
3864                        Token::RParen => Ok(e),
3865                        other => Err(ParseError {
3866                            message: format!("expected ')', got {other:?}"),
3867                            token_pos: self.pos.saturating_sub(1),
3868                        }),
3869                    }
3870                }
3871            }
3872            Token::LBracket => self.parse_vector_literal_body(),
3873            Token::Extract => self.parse_extract_atom(),
3874            Token::Interval => self.parse_interval_atom(),
3875            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
3876            // token; we match on the bare ident. NOT is a token
3877            // (consumed in the comparison rung), but `EXISTS (...)`
3878            // at the top of an expression starts here.
3879            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
3880                self.parse_exists_atom(false)
3881            }
3882            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
3883            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
3884            // CASE is a bare ident; we dispatch on lowercase match.
3885            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
3886                self.parse_case_atom()
3887            }
3888            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
3889            // is not a reserved token; we match by case-insensitive
3890            // ident. The opening `[` must follow immediately.
3891            Token::Ident(s) | Token::QuotedIdent(s)
3892                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
3893            {
3894                self.advance(); // consume `[`
3895                let mut items: Vec<Expr> = Vec::new();
3896                if !matches!(self.peek(), Token::RBracket) {
3897                    loop {
3898                        items.push(self.parse_expr(0)?);
3899                        match self.peek() {
3900                            Token::Comma => {
3901                                self.advance();
3902                            }
3903                            Token::RBracket => break,
3904                            other => {
3905                                return Err(self.err(alloc::format!(
3906                                    "expected ',' or ']' in ARRAY literal, got {other:?}"
3907                                )));
3908                            }
3909                        }
3910                    }
3911                }
3912                self.advance(); // consume `]`
3913                Ok(Expr::Array(items))
3914            }
3915            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
3916            other => Err(ParseError {
3917                message: format!("unexpected token {other:?} in expression"),
3918                token_pos: tok_pos,
3919            }),
3920        }
3921        // After parsing the atom, fold any postfix `::vector` casts.
3922        .and_then(|atom| self.finish_postfix_casts(atom))
3923    }
3924
3925    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
3926    /// Both bind tighter than any binary op.
3927    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
3928        loop {
3929            if matches!(self.peek(), Token::DoubleColon) {
3930                self.advance();
3931                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
3932                // target set to include INTERVAL (reserved Token),
3933                // TIMESTAMPTZ, and PG catalog regtype / regclass.
3934                // mailrs follow-up H3a + H3b.
3935                let target = match self.advance() {
3936                    Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
3937                        "int" | "integer" | "int4" => {
3938                            if matches!(self.peek(), Token::LBracket)
3939                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
3940                            {
3941                                self.advance();
3942                                self.advance();
3943                                CastTarget::IntArray
3944                            } else {
3945                                CastTarget::Int
3946                            }
3947                        }
3948                        "bigint" | "int8" => {
3949                            if matches!(self.peek(), Token::LBracket)
3950                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
3951                            {
3952                                self.advance();
3953                                self.advance();
3954                                CastTarget::BigIntArray
3955                            } else {
3956                                CastTarget::BigInt
3957                            }
3958                        }
3959                        "float" | "double" | "real" => CastTarget::Float,
3960                        "text" => {
3961                            // v7.10.11 — `::TEXT[]` widens to TextArray.
3962                            if matches!(self.peek(), Token::LBracket)
3963                                && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
3964                            {
3965                                self.advance();
3966                                self.advance();
3967                                CastTarget::TextArray
3968                            } else {
3969                                CastTarget::Text
3970                            }
3971                        }
3972                        "bool" | "boolean" => CastTarget::Bool,
3973                        "vector" => CastTarget::Vector,
3974                        "date" => CastTarget::Date,
3975                        "timestamp" | "datetime" => CastTarget::Timestamp,
3976                        "timestamptz" => CastTarget::Timestamptz,
3977                        "interval" => CastTarget::Interval,
3978                        "json" => CastTarget::Json,
3979                        "jsonb" => CastTarget::Jsonb,
3980                        "regtype" => CastTarget::RegType,
3981                        "regclass" => CastTarget::RegClass,
3982                        // v7.12.0 — `::tsvector` / `::tsquery`.
3983                        // Engine decodes the LHS text via the PG
3984                        // external form parser.
3985                        "tsvector" => CastTarget::TsVector,
3986                        "tsquery" => CastTarget::TsQuery,
3987                        other => {
3988                            return Err(ParseError {
3989                                message: format!("unsupported cast target `::{other}`"),
3990                                token_pos: self.pos.saturating_sub(1),
3991                            });
3992                        }
3993                    },
3994                    Token::Interval => CastTarget::Interval,
3995                    other => {
3996                        return Err(ParseError {
3997                            message: format!("expected type ident after `::`, got {other:?}"),
3998                            token_pos: self.pos.saturating_sub(1),
3999                        });
4000                    }
4001                };
4002                expr = Expr::Cast {
4003                    expr: Box::new(expr),
4004                    target,
4005                };
4006                continue;
4007            }
4008            if matches!(self.peek(), Token::Is) {
4009                self.advance();
4010                let negated = if matches!(self.peek(), Token::Not) {
4011                    self.advance();
4012                    true
4013                } else {
4014                    false
4015                };
4016                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
4017                // mailrs pg_dump.
4018                if matches!(self.peek(), Token::Distinct) {
4019                    self.advance();
4020                    if !matches!(self.peek(), Token::From) {
4021                        return Err(self.err(format!(
4022                            "expected FROM after IS{} DISTINCT, got {:?}",
4023                            if negated { " NOT" } else { "" },
4024                            self.peek()
4025                        )));
4026                    }
4027                    self.advance();
4028                    // Right-hand side: parse at the same precedence
4029                    // tier as comparison so `x IS DISTINCT FROM a + b`
4030                    // groups as `x IS DISTINCT FROM (a + b)`.
4031                    let rhs = self.parse_expr(20)?;
4032                    let op = if negated {
4033                        BinOp::IsNotDistinctFrom
4034                    } else {
4035                        BinOp::IsDistinctFrom
4036                    };
4037                    expr = Expr::Binary {
4038                        op,
4039                        lhs: Box::new(expr),
4040                        rhs: Box::new(rhs),
4041                    };
4042                    continue;
4043                }
4044                if !matches!(self.peek(), Token::Null) {
4045                    return Err(self.err(format!(
4046                        "expected NULL or DISTINCT after IS{}, got {:?}",
4047                        if negated { " NOT" } else { "" },
4048                        self.peek()
4049                    )));
4050                }
4051                self.advance();
4052                expr = Expr::IsNull {
4053                    expr: Box::new(expr),
4054                    negated,
4055                };
4056                continue;
4057            }
4058            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
4059            // Look one token ahead so a stray `NOT` not followed by any of
4060            // these flows through to the early return below untouched.
4061            let negated = if matches!(self.peek(), Token::Not) {
4062                let next = self.tokens.get(self.pos + 1);
4063                matches!(next, Some(Token::Between | Token::In | Token::Like))
4064            } else {
4065                false
4066            };
4067            if negated {
4068                self.advance();
4069            }
4070            if matches!(self.peek(), Token::Between) {
4071                expr = self.parse_between_tail(expr, negated)?;
4072                continue;
4073            }
4074            if matches!(self.peek(), Token::In) {
4075                expr = self.parse_in_tail(expr, negated)?;
4076                continue;
4077            }
4078            if matches!(self.peek(), Token::Like) {
4079                self.advance();
4080                // Pattern at the same precedence as other comparison RHSes —
4081                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
4082                let pattern = self.parse_expr(5)?;
4083                expr = Expr::Like {
4084                    expr: Box::new(expr),
4085                    pattern: Box::new(pattern),
4086                    negated,
4087                };
4088                continue;
4089            }
4090            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
4091            // returns NULL for out-of-range. Multiple subscripts
4092            // chain: `a[i][j]` parses left-to-right.
4093            if matches!(self.peek(), Token::LBracket) {
4094                self.advance();
4095                let index = self.parse_expr(0)?;
4096                if !matches!(self.peek(), Token::RBracket) {
4097                    return Err(self.err(alloc::format!(
4098                        "expected ']' after array index, got {:?}",
4099                        self.peek()
4100                    )));
4101                }
4102                self.advance();
4103                expr = Expr::ArraySubscript {
4104                    target: Box::new(expr),
4105                    index: Box::new(index),
4106                };
4107                continue;
4108            }
4109            return Ok(expr);
4110        }
4111    }
4112
4113    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
4114    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
4115    /// `AND` is not swallowed.
4116    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
4117        self.advance(); // BETWEEN
4118        let low = self.parse_expr(5)?;
4119        if !matches!(self.peek(), Token::And) {
4120            return Err(self.err(format!(
4121                "expected AND after BETWEEN low bound, got {:?}",
4122                self.peek()
4123            )));
4124        }
4125        self.advance();
4126        let high = self.parse_expr(5)?;
4127        let target = Box::new(expr);
4128        let combined = Expr::Binary {
4129            lhs: Box::new(Expr::Binary {
4130                lhs: target.clone(),
4131                op: BinOp::GtEq,
4132                rhs: Box::new(low),
4133            }),
4134            op: BinOp::And,
4135            rhs: Box::new(Expr::Binary {
4136                lhs: target,
4137                op: BinOp::LtEq,
4138                rhs: Box::new(high),
4139            }),
4140        };
4141        Ok(maybe_not(combined, negated))
4142    }
4143
4144    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
4145    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
4146    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
4147    /// Caller already consumed the leading `WITH` ident.
4148    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
4149        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
4150        // Comes through as an identifier; consume it if present and
4151        // mark every CTE in the clause as recursive (PG semantics —
4152        // the flag is per-WITH, not per-CTE).
4153        let mut recursive = false;
4154        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4155            && s.eq_ignore_ascii_case("recursive")
4156        {
4157            self.advance();
4158            recursive = true;
4159        }
4160        let mut ctes = Vec::new();
4161        loop {
4162            let name = self.expect_ident_like()?;
4163            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
4164            // PG uses these to rename the body's output columns; we
4165            // do the same below by overriding `columns[i].name`.
4166            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
4167                self.advance();
4168                let mut names = Vec::new();
4169                loop {
4170                    names.push(self.expect_ident_like()?);
4171                    if matches!(self.peek(), Token::Comma) {
4172                        self.advance();
4173                        continue;
4174                    }
4175                    break;
4176                }
4177                if !matches!(self.peek(), Token::RParen) {
4178                    return Err(self.err(format!(
4179                        "expected ')' to close CTE column list, got {:?}",
4180                        self.peek()
4181                    )));
4182                }
4183                self.advance();
4184                names
4185            } else {
4186                Vec::new()
4187            };
4188            // AS is a reserved Token::As (used by SELECT-item / FROM
4189            // aliasing) — handle it specially rather than as a bare
4190            // ident.
4191            if !matches!(self.peek(), Token::As) {
4192                return Err(self.err(format!(
4193                    "expected AS after CTE name {name:?}, got {:?}",
4194                    self.peek()
4195                )));
4196            }
4197            self.advance();
4198            if !matches!(self.peek(), Token::LParen) {
4199                return Err(self.err(format!(
4200                    "expected '(' after AS in WITH clause, got {:?}",
4201                    self.peek()
4202                )));
4203            }
4204            self.advance();
4205            if !matches!(self.peek(), Token::Select) {
4206                return Err(self.err(format!("WITH body must be a SELECT, got {:?}", self.peek())));
4207            }
4208            let inner = self.parse_select_stmt()?;
4209            if !matches!(self.peek(), Token::RParen) {
4210                return Err(self.err(format!(
4211                    "expected ')' after CTE body, got {:?}",
4212                    self.peek()
4213                )));
4214            }
4215            self.advance();
4216            let Statement::Select(body) = inner else {
4217                unreachable!("parse_select_stmt returns Select")
4218            };
4219            ctes.push(crate::ast::Cte {
4220                name,
4221                body,
4222                recursive,
4223                column_overrides,
4224            });
4225            if matches!(self.peek(), Token::Comma) {
4226                self.advance();
4227                continue;
4228            }
4229            break;
4230        }
4231        // The body SELECT follows. Must start with SELECT.
4232        if !matches!(self.peek(), Token::Select) {
4233            return Err(self.err(format!(
4234                "expected SELECT after WITH clause, got {:?}",
4235                self.peek()
4236            )));
4237        }
4238        let body_stmt = self.parse_select_stmt()?;
4239        let Statement::Select(mut body) = body_stmt else {
4240            unreachable!()
4241        };
4242        body.ctes = ctes;
4243        Ok(Statement::Select(body))
4244    }
4245
4246    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
4247    /// already consumed the leading `EXISTS` ident via
4248    /// `self.advance()`.
4249    /// v7.13.0 — parse the rest of a `CASE … END` expression after
4250    /// the leading `CASE` ident has been consumed (mailrs round-5
4251    /// G9). Supports both the searched form
4252    /// (`CASE WHEN cond THEN val …`) and the simple form
4253    /// (`CASE operand WHEN val THEN val …`).
4254    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
4255        // Disambiguate searched vs simple form: if the next token
4256        // is `WHEN`, we're in the searched form. Otherwise the
4257        // intervening expression is the operand.
4258        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
4259            None
4260        } else {
4261            Some(Box::new(self.parse_expr(0)?))
4262        };
4263        let mut branches: Vec<(Expr, Expr)> = Vec::new();
4264        loop {
4265            match self.peek() {
4266                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
4267                    self.advance();
4268                    let cond = self.parse_expr(0)?;
4269                    match self.peek() {
4270                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
4271                            self.advance();
4272                        }
4273                        other => {
4274                            return Err(self.err(alloc::format!(
4275                                "expected THEN after CASE WHEN <expr>, got {other:?}"
4276                            )));
4277                        }
4278                    }
4279                    let value = self.parse_expr(0)?;
4280                    branches.push((cond, value));
4281                }
4282                _ => break,
4283            }
4284        }
4285        if branches.is_empty() {
4286            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
4287        }
4288        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
4289        {
4290            self.advance();
4291            Some(Box::new(self.parse_expr(0)?))
4292        } else {
4293            None
4294        };
4295        match self.peek() {
4296            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
4297                self.advance();
4298            }
4299            other => {
4300                return Err(self.err(alloc::format!(
4301                    "expected END to close CASE expression, got {other:?}"
4302                )));
4303            }
4304        }
4305        Ok(Expr::Case {
4306            operand,
4307            branches,
4308            else_branch,
4309        })
4310    }
4311
4312    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
4313        if !matches!(self.peek(), Token::LParen) {
4314            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
4315        }
4316        self.advance();
4317        let inner = self.parse_select_stmt()?;
4318        if !matches!(self.peek(), Token::RParen) {
4319            return Err(self.err(format!(
4320                "expected ')' after EXISTS-subquery, got {:?}",
4321                self.peek()
4322            )));
4323        }
4324        self.advance();
4325        let Statement::Select(s) = inner else {
4326            unreachable!("parse_select_stmt returns Select")
4327        };
4328        Ok(Expr::Exists {
4329            subquery: Box::new(s),
4330            negated,
4331        })
4332    }
4333
4334    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
4335        self.advance(); // IN
4336        if !matches!(self.peek(), Token::LParen) {
4337            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
4338        }
4339        self.advance();
4340        // v4.10: `IN (SELECT ...)` — subquery branch.
4341        if matches!(self.peek(), Token::Select) {
4342            let inner = self.parse_select_stmt()?;
4343            if !matches!(self.peek(), Token::RParen) {
4344                return Err(self.err(format!(
4345                    "expected ')' after IN-subquery, got {:?}",
4346                    self.peek()
4347                )));
4348            }
4349            self.advance();
4350            let Statement::Select(s) = inner else {
4351                unreachable!("parse_select_stmt always returns Statement::Select")
4352            };
4353            return Ok(Expr::InSubquery {
4354                expr: Box::new(expr),
4355                subquery: Box::new(s),
4356                negated,
4357            });
4358        }
4359        let mut elements = Vec::new();
4360        if !matches!(self.peek(), Token::RParen) {
4361            loop {
4362                elements.push(self.parse_expr(0)?);
4363                match self.peek() {
4364                    Token::Comma => {
4365                        self.advance();
4366                    }
4367                    Token::RParen => break,
4368                    other => {
4369                        return Err(
4370                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
4371                        );
4372                    }
4373                }
4374            }
4375        }
4376        self.advance(); // ')'
4377        let target = Box::new(expr);
4378        let combined = if elements.is_empty() {
4379            Expr::Literal(Literal::Bool(false))
4380        } else {
4381            let mut iter = elements.into_iter();
4382            let first = iter.next().unwrap();
4383            let mut acc = Expr::Binary {
4384                lhs: target.clone(),
4385                op: BinOp::Eq,
4386                rhs: Box::new(first),
4387            };
4388            for elt in iter {
4389                acc = Expr::Binary {
4390                    lhs: Box::new(acc),
4391                    op: BinOp::Or,
4392                    rhs: Box::new(Expr::Binary {
4393                        lhs: target.clone(),
4394                        op: BinOp::Eq,
4395                        rhs: Box::new(elt),
4396                    }),
4397                };
4398            }
4399            acc
4400        };
4401        Ok(maybe_not(combined, negated))
4402    }
4403
4404    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
4405    /// already consumed by the caller. Elements must be numeric literals
4406    /// (with optional unary `-`); any compound expression is rejected at
4407    /// parse time so the runtime never needs to evaluate inside a vector.
4408    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
4409    /// has already consumed the `EXTRACT` token before calling us —
4410    /// we pick up at the opening `(`.
4411    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
4412        if !matches!(self.peek(), Token::LParen) {
4413            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
4414        }
4415        self.advance();
4416        let field_name = self.expect_ident_like()?;
4417        let field = match field_name.to_ascii_lowercase().as_str() {
4418            "year" => ExtractField::Year,
4419            "month" => ExtractField::Month,
4420            "day" => ExtractField::Day,
4421            "hour" => ExtractField::Hour,
4422            "minute" => ExtractField::Minute,
4423            "second" => ExtractField::Second,
4424            "microsecond" | "microseconds" => ExtractField::Microsecond,
4425            other => {
4426                return Err(self.err(format!(
4427                    "unknown EXTRACT field {other:?}; \
4428                     supported: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MICROSECOND"
4429                )));
4430            }
4431        };
4432        if !matches!(self.peek(), Token::From) {
4433            return Err(self.err(format!(
4434                "expected FROM after EXTRACT field, got {:?}",
4435                self.peek()
4436            )));
4437        }
4438        self.advance();
4439        let source = self.parse_expr(0)?;
4440        if !matches!(self.peek(), Token::RParen) {
4441            return Err(self.err(format!(
4442                "expected ')' to close EXTRACT, got {:?}",
4443                self.peek()
4444            )));
4445        }
4446        self.advance();
4447        Ok(Expr::Extract {
4448            field,
4449            source: Box::new(source),
4450        })
4451    }
4452
4453    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
4454    /// is already consumed; we expect a single string literal next and
4455    /// resolve it into `Literal::Interval` at parse time so the engine
4456    /// never has to re-tokenise inside the string.
4457    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
4458        let tok = self.advance();
4459        let Token::String(text) = tok else {
4460            return Err(self.err(format!(
4461                "expected string literal after INTERVAL, got {tok:?}"
4462            )));
4463        };
4464        let (months, micros) = parse_interval_text(&text).ok_or_else(|| ParseError {
4465            message: format!(
4466                "cannot parse INTERVAL {text:?}; \
4467                     expected `<n> <unit> [<n> <unit> ...]` with units \
4468                     microsecond[s], millisecond[s], second[s], minute[s], \
4469                     hour[s], day[s], week[s], month[s], year[s]"
4470            ),
4471            token_pos: self.pos.saturating_sub(1),
4472        })?;
4473        Ok(Expr::Literal(Literal::Interval {
4474            months,
4475            micros,
4476            text,
4477        }))
4478    }
4479
4480    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
4481        let mut elems = Vec::new();
4482        if matches!(self.peek(), Token::RBracket) {
4483            self.advance();
4484            return Ok(Expr::Literal(Literal::Vector(elems)));
4485        }
4486        loop {
4487            let e = self.parse_expr(0)?;
4488            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
4489                message: format!("vector element must be a numeric literal, got {e:?}"),
4490                token_pos: self.pos,
4491            })?;
4492            elems.push(x);
4493            match self.peek() {
4494                Token::Comma => {
4495                    self.advance();
4496                }
4497                Token::RBracket => {
4498                    self.advance();
4499                    break;
4500                }
4501                other => {
4502                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
4503                }
4504            }
4505        }
4506        Ok(Expr::Literal(Literal::Vector(elems)))
4507    }
4508
4509    /// Atom that started with an identifier: could be `t.col`, `col`, or
4510    /// `func(arg, ...)`. Detect each shape by looking at the next token.
4511    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
4512    /// [, ...])`. Caller has already consumed `OVER`. Either clause
4513    /// is optional; an empty `()` is also legal (PG semantics).
4514    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
4515    /// modifier between `name(args)` and `OVER (...)`. Default is
4516    /// `Respect`. Unrecognised idents leave the stream unchanged.
4517    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
4518        let Token::Ident(s) = self.peek().clone() else {
4519            return NullTreatment::Respect;
4520        };
4521        let is_ignore = s.eq_ignore_ascii_case("ignore");
4522        let is_respect = s.eq_ignore_ascii_case("respect");
4523        if !is_ignore && !is_respect {
4524            return NullTreatment::Respect;
4525        }
4526        // Lookahead for NULLS — only consume both tokens together.
4527        // pos+1 must hold a "nulls" ident.
4528        if self.pos + 1 < self.tokens.len()
4529            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
4530            && s2.eq_ignore_ascii_case("nulls")
4531        {
4532            self.advance();
4533            self.advance();
4534            return if is_ignore {
4535                NullTreatment::Ignore
4536            } else {
4537                NullTreatment::Respect
4538            };
4539        }
4540        NullTreatment::Respect
4541    }
4542
4543    /// No frame clause is supported.
4544    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
4545    fn parse_over_clause(
4546        &mut self,
4547    ) -> Result<(Vec<Expr>, Vec<(Expr, bool)>, Option<WindowFrame>), ParseError> {
4548        if !matches!(self.peek(), Token::LParen) {
4549            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
4550        }
4551        self.advance();
4552        let mut partition_by = Vec::new();
4553        let mut order_by = Vec::new();
4554        // PARTITION BY ?
4555        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4556            && s.eq_ignore_ascii_case("partition")
4557        {
4558            self.advance();
4559            if !matches!(self.peek(), Token::By) {
4560                return Err(self.err(format!(
4561                    "expected BY after PARTITION, got {:?}",
4562                    self.peek()
4563                )));
4564            }
4565            self.advance();
4566            loop {
4567                partition_by.push(self.parse_expr(0)?);
4568                if matches!(self.peek(), Token::Comma) {
4569                    self.advance();
4570                    continue;
4571                }
4572                break;
4573            }
4574        }
4575        // ORDER BY ?
4576        if matches!(self.peek(), Token::Order) {
4577            self.advance();
4578            if !matches!(self.peek(), Token::By) {
4579                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
4580            }
4581            self.advance();
4582            loop {
4583                let e = self.parse_expr(0)?;
4584                let desc = if matches!(self.peek(), Token::Desc) {
4585                    self.advance();
4586                    true
4587                } else if matches!(self.peek(), Token::Asc) {
4588                    self.advance();
4589                    false
4590                } else {
4591                    false
4592                };
4593                order_by.push((e, desc));
4594                if matches!(self.peek(), Token::Comma) {
4595                    self.advance();
4596                    continue;
4597                }
4598                break;
4599            }
4600        }
4601        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
4602        // Both keywords come through the lexer as identifiers; match
4603        // case-insensitively.
4604        let mut frame: Option<WindowFrame> = None;
4605        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4606            let kind = if s.eq_ignore_ascii_case("rows") {
4607                Some(FrameKind::Rows)
4608            } else if s.eq_ignore_ascii_case("range") {
4609                Some(FrameKind::Range)
4610            } else {
4611                None
4612            };
4613            if let Some(kind) = kind {
4614                self.advance();
4615                frame = Some(self.parse_frame_tail(kind)?);
4616            }
4617        }
4618        if !matches!(self.peek(), Token::RParen) {
4619            return Err(self.err(format!(
4620                "expected ')' to close OVER clause, got {:?}",
4621                self.peek()
4622            )));
4623        }
4624        self.advance();
4625        Ok((partition_by, order_by, frame))
4626    }
4627
4628    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
4629    /// or `RANGE` keyword was just consumed. Accepts both
4630    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
4631    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
4632    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
4633    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
4634        if matches!(self.peek(), Token::Between) {
4635            self.advance();
4636            let start = self.parse_frame_bound()?;
4637            if !matches!(self.peek(), Token::And) {
4638                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
4639            }
4640            self.advance();
4641            let end = self.parse_frame_bound()?;
4642            Ok(WindowFrame {
4643                kind,
4644                start,
4645                end: Some(end),
4646            })
4647        } else {
4648            let start = self.parse_frame_bound()?;
4649            Ok(WindowFrame {
4650                kind,
4651                start,
4652                end: None,
4653            })
4654        }
4655    }
4656
4657    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
4658    /// `CURRENT ROW`, `<n> FOLLOWING`, `UNBOUNDED FOLLOWING`.
4659    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
4660        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
4661        if let Token::Integer(n) = *self.peek() {
4662            self.advance();
4663            let n: u64 = u64::try_from(n).map_err(|_| {
4664                self.err(format!(
4665                    "invalid frame offset {n} — expected non-negative integer"
4666                ))
4667            })?;
4668            let dir = self.expect_ident_like()?;
4669            return if dir.eq_ignore_ascii_case("preceding") {
4670                Ok(FrameBound::OffsetPreceding(n))
4671            } else if dir.eq_ignore_ascii_case("following") {
4672                Ok(FrameBound::OffsetFollowing(n))
4673            } else {
4674                Err(self.err(format!(
4675                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
4676                )))
4677            };
4678        }
4679        let first = self.expect_ident_like()?;
4680        if first.eq_ignore_ascii_case("unbounded") {
4681            let dir = self.expect_ident_like()?;
4682            return if dir.eq_ignore_ascii_case("preceding") {
4683                Ok(FrameBound::UnboundedPreceding)
4684            } else if dir.eq_ignore_ascii_case("following") {
4685                Ok(FrameBound::UnboundedFollowing)
4686            } else {
4687                Err(self.err(format!(
4688                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
4689                )))
4690            };
4691        }
4692        if first.eq_ignore_ascii_case("current") {
4693            let row = self.expect_ident_like()?;
4694            if !row.eq_ignore_ascii_case("row") {
4695                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
4696            }
4697            return Ok(FrameBound::CurrentRow);
4698        }
4699        Err(self.err(format!(
4700            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
4701        )))
4702    }
4703
4704    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
4705        if matches!(self.peek(), Token::Dot) {
4706            self.advance();
4707            let name = self.expect_ident_like()?;
4708            return Ok(Expr::Column(ColumnName {
4709                qualifier: Some(first),
4710                name,
4711            }));
4712        }
4713        if matches!(self.peek(), Token::LParen) {
4714            self.advance();
4715            // `COUNT(*)` — special-cased here because `*` isn't a normal
4716            // expression token. Lower-case match on `first` since the lexer
4717            // folds identifiers.
4718            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
4719                self.advance();
4720                if !matches!(self.peek(), Token::RParen) {
4721                    return Err(self.err(format!(
4722                        "expected ')' after COUNT(*), got {:?}",
4723                        self.peek()
4724                    )));
4725                }
4726                self.advance();
4727                // v4.12: COUNT(*) OVER (...) — same window tail.
4728                let null_treatment = self.parse_null_treatment_modifier();
4729                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4730                    && s.eq_ignore_ascii_case("over")
4731                {
4732                    self.advance();
4733                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
4734                    return Ok(Expr::WindowFunction {
4735                        name: "count_star".into(),
4736                        args: Vec::new(),
4737                        partition_by,
4738                        order_by,
4739                        frame,
4740                        null_treatment,
4741                    });
4742                }
4743                return Ok(Expr::FunctionCall {
4744                    name: "count_star".into(),
4745                    args: Vec::new(),
4746                });
4747            }
4748            // Function call. PG-style: zero-or-more comma-separated args.
4749            let mut args = Vec::new();
4750            if !matches!(self.peek(), Token::RParen) {
4751                loop {
4752                    args.push(self.parse_expr(0)?);
4753                    match self.peek() {
4754                        Token::Comma => {
4755                            self.advance();
4756                        }
4757                        Token::RParen => break,
4758                        other => {
4759                            return Err(self.err(format!(
4760                                "expected ',' or ')' in function args, got {other:?}"
4761                            )));
4762                        }
4763                    }
4764                }
4765            }
4766            self.advance(); // consume ')'
4767            // v4.12: window-function tail — `name(args) OVER (...)`.
4768            // Promotes the just-parsed FunctionCall into a
4769            // WindowFunction node carrying partition + order.
4770            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
4771            // / `RESPECT NULLS OVER (...)` between the closing paren
4772            // and `OVER`.
4773            let null_treatment = self.parse_null_treatment_modifier();
4774            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
4775                && s.eq_ignore_ascii_case("over")
4776            {
4777                self.advance();
4778                let (partition_by, order_by, frame) = self.parse_over_clause()?;
4779                return Ok(Expr::WindowFunction {
4780                    name: first,
4781                    args,
4782                    partition_by,
4783                    order_by,
4784                    frame,
4785                    null_treatment,
4786                });
4787            }
4788            return Ok(Expr::FunctionCall { name: first, args });
4789        }
4790        // v7.9.20 — SQL-standard parenless keyword expressions
4791        // (PG treats these as functions called without parens).
4792        // Resolve to a synthetic FunctionCall so the engine's
4793        // eval path reuses the existing function-call routing.
4794        // mailrs G3.
4795        let lc = first.to_ascii_lowercase();
4796        if matches!(
4797            lc.as_str(),
4798            "current_date" | "current_time" | "current_timestamp" | "localtimestamp" | "localtime"
4799        ) {
4800            return Ok(Expr::FunctionCall {
4801                name: lc,
4802                args: Vec::new(),
4803            });
4804        }
4805        Ok(Expr::Column(ColumnName {
4806            qualifier: None,
4807            name: first,
4808        }))
4809    }
4810}
4811
4812/// v6.8.2 — walk an expression tree and return the first column
4813/// reference's bare name. Used by `parse_create_index_stmt_after_create`
4814/// to derive `CreateIndexStatement.column` from an expression
4815/// key (so downstream planner code resolving a primary column
4816/// position keeps working with expression indexes). Returns
4817/// `None` when the expression has no column ref at all — caller
4818/// surfaces that as a parse error.
4819fn extract_first_column(expr: &Expr) -> Option<String> {
4820    match expr {
4821        Expr::Column(cn) => Some(cn.name.clone()),
4822        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
4823        Expr::Binary { lhs, rhs, .. } => {
4824            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
4825        }
4826        Expr::Unary { expr: e, .. } => extract_first_column(e),
4827        _ => None,
4828    }
4829}
4830
4831fn maybe_not(expr: Expr, negated: bool) -> Expr {
4832    if negated {
4833        Expr::Unary {
4834            op: UnOp::Not,
4835            expr: Box::new(expr),
4836        }
4837    } else {
4838        expr
4839    }
4840}
4841
4842fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
4843    let pair = match tok {
4844        Token::Or => (BinOp::Or, 1),
4845        Token::And => (BinOp::And, 2),
4846        Token::Eq => (BinOp::Eq, 4),
4847        Token::NotEq => (BinOp::NotEq, 4),
4848        Token::Lt => (BinOp::Lt, 4),
4849        Token::LtEq => (BinOp::LtEq, 4),
4850        Token::Gt => (BinOp::Gt, 4),
4851        Token::GtEq => (BinOp::GtEq, 4),
4852        // pgvector distance ops all sit on the same rung — tighter than
4853        // comparisons (4) so `col <-> v < threshold` parses correctly.
4854        Token::L2Distance => (BinOp::L2Distance, 5),
4855        Token::InnerProduct => (BinOp::InnerProduct, 5),
4856        Token::CosineDistance => (BinOp::CosineDistance, 5),
4857        Token::Plus => (BinOp::Add, 6),
4858        Token::Minus => (BinOp::Sub, 6),
4859        // `||` sits beside `+`/`-` (matches PG conceptually — concat groups
4860        // by the same level as binary additive arithmetic).
4861        Token::Concat => (BinOp::Concat, 6),
4862        Token::Star => (BinOp::Mul, 7),
4863        Token::Slash => (BinOp::Div, 7),
4864        // v4.14: JSON path ops bind tighter than comparisons (4)
4865        // and additive (6) so `doc->'k' = 'v'` parses correctly.
4866        // Same rung as the multiplicative ops.
4867        Token::JsonGet => (BinOp::JsonGet, 7),
4868        Token::JsonGetText => (BinOp::JsonGetText, 7),
4869        Token::JsonGetPath => (BinOp::JsonGetPath, 7),
4870        Token::JsonGetPathText => (BinOp::JsonGetPathText, 7),
4871        Token::JsonContains => (BinOp::JsonContains, 7),
4872        // v7.12.2 — `@@` binds at the comparison rung (looser than
4873        // arithmetic, tighter than AND / OR). PG places `@@` at
4874        // the same precedence as `=` / `<`, so we follow.
4875        Token::TsMatch => (BinOp::TsMatch, 4),
4876        _ => return None,
4877    };
4878    Some(pair)
4879}
4880
4881#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4882// `as f32` here is intentional: vector elements widen / narrow into f32 on
4883// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
4884// past ~15 decimal digits — both are acceptable for a fixed-precision
4885// pgvector column.
4886fn extract_numeric_literal(e: &Expr) -> Option<f32> {
4887    match e {
4888        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
4889        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
4890        Expr::Unary {
4891            op: UnOp::Neg,
4892            expr,
4893        } => extract_numeric_literal(expr).map(|x| -x),
4894        _ => None,
4895    }
4896}
4897
4898/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
4899/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
4900/// negative. Returns `None` if any pair fails to parse or no pair is found.
4901///
4902/// Recognised units (case-insensitive, optional trailing `s`):
4903/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
4904/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
4905pub fn parse_interval_text(s: &str) -> Option<(i32, i64)> {
4906    let parts: Vec<&str> = s.split_whitespace().collect();
4907    if parts.is_empty() || !parts.len().is_multiple_of(2) {
4908        return None;
4909    }
4910    let mut months: i32 = 0;
4911    let mut micros: i64 = 0;
4912    let mut i = 0;
4913    while i < parts.len() {
4914        let n: i64 = parts[i].parse().ok()?;
4915        let unit = parts[i + 1].to_ascii_lowercase();
4916        let unit_stripped = unit.strip_suffix('s').unwrap_or(&unit);
4917        match unit_stripped {
4918            "microsecond" => micros = micros.checked_add(n)?,
4919            "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
4920            "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
4921            "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
4922            "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
4923            "day" => micros = micros.checked_add(n.checked_mul(86_400_000_000)?)?,
4924            "week" => micros = micros.checked_add(n.checked_mul(604_800_000_000)?)?,
4925            "month" => {
4926                let n32 = i32::try_from(n).ok()?;
4927                months = months.checked_add(n32)?;
4928            }
4929            "year" => {
4930                let n32 = i32::try_from(n).ok()?;
4931                months = months.checked_add(n32.checked_mul(12)?)?;
4932            }
4933            _ => return None,
4934        }
4935        i += 2;
4936    }
4937    Some((months, micros))
4938}
4939
4940/// v7.12.4 — map a bare type-name identifier (the form that
4941/// appears in a function arg list or RETURNS clause) to a
4942/// [`ColumnTypeName`]. Returns `None` for unknown / extension
4943/// types so the caller can preserve them as
4944/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
4945///
4946/// Subset of the full column-type grammar — we deliberately
4947/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
4948/// here because function-arg types in v7.12.4 are mostly the
4949/// bare form (`text`, `int`, `bytea`, …).
4950fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
4951    Some(match ident.to_ascii_lowercase().as_str() {
4952        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
4953        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
4954        "bigint" => ColumnTypeName::BigInt,
4955        "float" | "double" | "real" => ColumnTypeName::Float,
4956        "text" => ColumnTypeName::Text,
4957        "bool" | "boolean" => ColumnTypeName::Bool,
4958        "date" => ColumnTypeName::Date,
4959        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
4960        "timestamptz" => ColumnTypeName::Timestamptz,
4961        "json" => ColumnTypeName::Json,
4962        "jsonb" => ColumnTypeName::Jsonb,
4963        "bytea" | "bytes" => ColumnTypeName::Bytes,
4964        "tsvector" => ColumnTypeName::TsVector,
4965        "tsquery" => ColumnTypeName::TsQuery,
4966        _ => return None,
4967    })
4968}
4969
4970/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
4971/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
4972///
4973/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
4974/// / embedded SQL land in v7.12.5+):
4975///
4976/// ```text
4977///   body          := [ws] block [ws]
4978///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
4979///   stmt          := assign | return
4980///   assign        := assign_target := expr
4981///   assign_target := ( NEW | OLD ) . ident | ident
4982///   return        := RETURN ( NEW | OLD | NULL | expr )
4983/// ```
4984///
4985/// `expr` is parsed by recursing into the regular `Parser` — so a
4986/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
4987/// NEW.subject || ' ' || NEW.sender)` body shape works without
4988/// the body parser knowing what `to_tsvector` is.
4989///
4990/// Errors here cause the caller to fall back to
4991/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
4992/// successful, but the executor will refuse to invoke the
4993/// function with an "unparseable body" error.
4994/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
4995/// from the crate root as `spg_sql::parse_function_body`.
4996pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
4997    parse_plpgsql_body(body)
4998}
4999
5000fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
5001    // Use the regular lexer on the body text. The trailing
5002    // `END;` may or may not have a semicolon; the lexer treats
5003    // both forms identically.
5004    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
5005        message: alloc::format!("plpgsql body lex error: {e}"),
5006        token_pos: 0,
5007    })?;
5008    let mut parser = Parser::new(tokens);
5009    parser.parse_plpgsql_block()
5010}
5011
5012#[cfg(test)]
5013mod tests {
5014    use super::*;
5015    use alloc::string::ToString;
5016
5017    fn parse(s: &str) -> Statement {
5018        parse_statement(s).expect("parse ok")
5019    }
5020
5021    fn lit_int(n: i64) -> Expr {
5022        Expr::Literal(Literal::Integer(n))
5023    }
5024
5025    fn col(name: &str) -> Expr {
5026        Expr::Column(ColumnName {
5027            qualifier: None,
5028            name: name.into(),
5029        })
5030    }
5031
5032    #[test]
5033    fn select_single_integer() {
5034        let s = parse("SELECT 1");
5035        let Statement::Select(s) = s else {
5036            panic!("expected SELECT")
5037        };
5038        assert_eq!(s.items.len(), 1);
5039        assert!(s.from.is_none());
5040        assert!(s.where_.is_none());
5041    }
5042
5043    #[test]
5044    fn select_multiple_literal_kinds() {
5045        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
5046        let Statement::Select(s) = s else {
5047            panic!("expected SELECT")
5048        };
5049        assert_eq!(s.items.len(), 5);
5050    }
5051
5052    #[test]
5053    fn select_wildcard_from_table() {
5054        let s = parse("SELECT * FROM users");
5055        let Statement::Select(s) = s else {
5056            panic!("expected SELECT")
5057        };
5058        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
5059        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
5060    }
5061
5062    #[test]
5063    fn select_with_table_alias() {
5064        let s = parse("SELECT * FROM users AS u");
5065        let Statement::Select(s) = s else {
5066            panic!("expected SELECT")
5067        };
5068        let t = &s.from.as_ref().unwrap().primary;
5069        assert_eq!(t.name, "users");
5070        assert_eq!(t.alias.as_deref(), Some("u"));
5071    }
5072
5073    #[test]
5074    fn select_with_where_eq() {
5075        let s = parse("SELECT a FROM t WHERE a = 1");
5076        let Statement::Select(s) = s else {
5077            panic!("expected SELECT")
5078        };
5079        let w = s.where_.unwrap();
5080        assert_eq!(
5081            w,
5082            Expr::Binary {
5083                lhs: Box::new(col("a")),
5084                op: BinOp::Eq,
5085                rhs: Box::new(lit_int(1)),
5086            }
5087        );
5088    }
5089
5090    #[test]
5091    fn arithmetic_precedence() {
5092        let s = parse("SELECT 1 + 2 * 3");
5093        let Statement::Select(s) = s else {
5094            panic!("expected SELECT")
5095        };
5096        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5097            panic!("wildcard?")
5098        };
5099        assert_eq!(
5100            expr,
5101            &Expr::Binary {
5102                lhs: Box::new(lit_int(1)),
5103                op: BinOp::Add,
5104                rhs: Box::new(Expr::Binary {
5105                    lhs: Box::new(lit_int(2)),
5106                    op: BinOp::Mul,
5107                    rhs: Box::new(lit_int(3)),
5108                }),
5109            }
5110        );
5111    }
5112
5113    #[test]
5114    fn parentheses_override_precedence() {
5115        let s = parse("SELECT (1 + 2) * 3");
5116        let Statement::Select(s) = s else {
5117            panic!("expected SELECT")
5118        };
5119        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5120            panic!()
5121        };
5122        assert_eq!(
5123            expr,
5124            &Expr::Binary {
5125                lhs: Box::new(Expr::Binary {
5126                    lhs: Box::new(lit_int(1)),
5127                    op: BinOp::Add,
5128                    rhs: Box::new(lit_int(2)),
5129                }),
5130                op: BinOp::Mul,
5131                rhs: Box::new(lit_int(3)),
5132            }
5133        );
5134    }
5135
5136    #[test]
5137    fn not_binds_below_comparison() {
5138        // `NOT a = 1` should parse as `NOT (a = 1)`.
5139        let s = parse("SELECT NOT a = 1 FROM t");
5140        let Statement::Select(s) = s else {
5141            panic!("expected SELECT")
5142        };
5143        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5144            panic!()
5145        };
5146        assert_eq!(
5147            expr,
5148            &Expr::Unary {
5149                op: UnOp::Not,
5150                expr: Box::new(Expr::Binary {
5151                    lhs: Box::new(col("a")),
5152                    op: BinOp::Eq,
5153                    rhs: Box::new(lit_int(1)),
5154                }),
5155            }
5156        );
5157    }
5158
5159    #[test]
5160    fn unary_minus_binds_above_multiplication() {
5161        // `-a * 2` should be `(-a) * 2`.
5162        let s = parse("SELECT -a * 2 FROM t");
5163        let Statement::Select(s) = s else {
5164            panic!("expected SELECT")
5165        };
5166        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5167            panic!()
5168        };
5169        assert_eq!(
5170            expr,
5171            &Expr::Binary {
5172                lhs: Box::new(Expr::Unary {
5173                    op: UnOp::Neg,
5174                    expr: Box::new(col("a")),
5175                }),
5176                op: BinOp::Mul,
5177                rhs: Box::new(lit_int(2)),
5178            }
5179        );
5180    }
5181
5182    #[test]
5183    fn qualified_column() {
5184        let s = parse("SELECT t.col FROM t");
5185        let Statement::Select(s) = s else {
5186            panic!("expected SELECT")
5187        };
5188        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5189            panic!()
5190        };
5191        assert_eq!(
5192            expr,
5193            &Expr::Column(ColumnName {
5194                qualifier: Some("t".into()),
5195                name: "col".into()
5196            })
5197        );
5198    }
5199
5200    #[test]
5201    fn select_item_alias_with_as() {
5202        let s = parse("SELECT a AS y FROM t");
5203        let Statement::Select(s) = s else {
5204            panic!("expected SELECT")
5205        };
5206        let SelectItem::Expr { alias, .. } = &s.items[0] else {
5207            panic!()
5208        };
5209        assert_eq!(alias.as_deref(), Some("y"));
5210    }
5211
5212    #[test]
5213    fn trailing_semicolon_accepted() {
5214        let s = parse("SELECT 1;");
5215        let Statement::Select(s) = s else {
5216            panic!("expected SELECT")
5217        };
5218        assert_eq!(s.items.len(), 1);
5219    }
5220
5221    #[test]
5222    fn boolean_chain_with_and_or_not() {
5223        // (NOT a) OR (b AND (NOT c))
5224        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
5225        let Statement::Select(s) = s else {
5226            panic!("expected SELECT")
5227        };
5228        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5229            panic!()
5230        };
5231        let expected = Expr::Binary {
5232            lhs: Box::new(Expr::Unary {
5233                op: UnOp::Not,
5234                expr: Box::new(col("a")),
5235            }),
5236            op: BinOp::Or,
5237            rhs: Box::new(Expr::Binary {
5238                lhs: Box::new(col("b")),
5239                op: BinOp::And,
5240                rhs: Box::new(Expr::Unary {
5241                    op: UnOp::Not,
5242                    expr: Box::new(col("c")),
5243                }),
5244            }),
5245        };
5246        assert_eq!(expr, &expected);
5247    }
5248
5249    #[test]
5250    fn empty_input_errors() {
5251        let err = parse_statement("").unwrap_err();
5252        assert!(err.message.contains("SELECT"));
5253    }
5254
5255    #[test]
5256    fn unmatched_paren_errors() {
5257        assert!(parse_statement("SELECT (1 + 2").is_err());
5258    }
5259
5260    #[test]
5261    fn display_round_trip_simple_select() {
5262        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
5263        let text = original.to_string();
5264        let again = parse_statement(&text).expect("re-parse");
5265        assert_eq!(original, again);
5266    }
5267
5268    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
5269
5270    #[test]
5271    fn create_table_single_column() {
5272        let s = parse("CREATE TABLE foo (a INT)");
5273        let Statement::CreateTable(c) = s else {
5274            panic!("expected CreateTable")
5275        };
5276        assert_eq!(c.name, "foo");
5277        assert_eq!(c.columns.len(), 1);
5278        assert_eq!(c.columns[0].name, "a");
5279        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
5280        assert!(c.columns[0].nullable);
5281    }
5282
5283    #[test]
5284    fn create_table_multi_column_with_not_null_mix() {
5285        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
5286        let Statement::CreateTable(c) = s else {
5287            panic!()
5288        };
5289        assert_eq!(c.columns.len(), 4);
5290        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
5291        assert!(!c.columns[0].nullable);
5292        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
5293        assert!(c.columns[1].nullable);
5294        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
5295        assert!(!c.columns[2].nullable);
5296        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
5297    }
5298
5299    #[test]
5300    fn create_table_bigint_supported() {
5301        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
5302        let Statement::CreateTable(c) = s else {
5303            panic!()
5304        };
5305        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
5306    }
5307
5308    #[test]
5309    fn create_table_vector_default_is_f32() {
5310        let s = parse("CREATE TABLE t (v VECTOR(128))");
5311        let Statement::CreateTable(c) = s else {
5312            panic!()
5313        };
5314        assert_eq!(
5315            c.columns[0].ty,
5316            ColumnTypeName::Vector {
5317                dim: 128,
5318                encoding: VecEncoding::F32,
5319            },
5320        );
5321    }
5322
5323    #[test]
5324    fn create_table_vector_using_sq8() {
5325        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
5326        // Case-insensitive on both `USING` and the encoding name.
5327        for sql in [
5328            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
5329            "CREATE TABLE t (v VECTOR(128) using sq8)",
5330        ] {
5331            let s = parse(sql);
5332            let Statement::CreateTable(c) = s else {
5333                panic!()
5334            };
5335            assert_eq!(
5336                c.columns[0].ty,
5337                ColumnTypeName::Vector {
5338                    dim: 128,
5339                    encoding: VecEncoding::Sq8,
5340                },
5341                "{sql}",
5342            );
5343        }
5344    }
5345
5346    #[test]
5347    fn create_table_vector_using_unknown_errors() {
5348        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
5349        assert!(
5350            err.message.contains("unknown vector encoding"),
5351            "got: {}",
5352            err.message
5353        );
5354    }
5355
5356    #[test]
5357    fn vector_using_sq8_display_roundtrips() {
5358        // The Display impl must produce text that re-parses to the
5359        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
5360        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
5361        let Statement::CreateTable(c) = s else {
5362            panic!()
5363        };
5364        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
5365    }
5366
5367    #[test]
5368    fn parser_recognises_placeholders() {
5369        use crate::ast::{Expr, SelectItem, Statement};
5370        // $N in expression position parses as Expr::Placeholder(N).
5371        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
5372        let Statement::Select(sel) = s else { panic!() };
5373        assert!(matches!(
5374            sel.items[0],
5375            SelectItem::Expr {
5376                expr: Expr::Placeholder(1),
5377                alias: None
5378            }
5379        ));
5380        // $2 + 1
5381        let SelectItem::Expr {
5382            expr: Expr::Binary { lhs, rhs, .. },
5383            ..
5384        } = &sel.items[1]
5385        else {
5386            panic!()
5387        };
5388        assert!(matches!(**lhs, Expr::Placeholder(2)));
5389        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
5390        // WHERE x = $3
5391        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
5392            panic!()
5393        };
5394        assert!(matches!(**rhs, Expr::Placeholder(3)));
5395    }
5396
5397    #[test]
5398    fn parser_rejects_dollar_zero() {
5399        // $0 is not valid in PG; the lexer rejects it.
5400        assert!(parse_statement("SELECT $0").is_err());
5401    }
5402
5403    #[test]
5404    fn placeholder_display_roundtrips() {
5405        // The Display impl must produce text that re-lexes to the
5406        // same Placeholder token.
5407        let s = parse("SELECT $42 FROM t");
5408        let printed = s.to_string();
5409        assert!(printed.contains("$42"));
5410        let again = parse(&printed);
5411        assert_eq!(s, again);
5412    }
5413
5414    #[test]
5415    fn alter_index_rebuild_bare() {
5416        use crate::ast::{AlterIndexTarget, Statement};
5417        let s = parse("ALTER INDEX my_idx REBUILD");
5418        let Statement::AlterIndex(a) = s else {
5419            panic!("expected AlterIndex, got {s:?}")
5420        };
5421        assert_eq!(a.name, "my_idx");
5422        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
5423    }
5424
5425    #[test]
5426    fn alter_index_rebuild_with_encoding() {
5427        use crate::ast::{AlterIndexTarget, Statement};
5428        for (sql, want) in [
5429            (
5430                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
5431                VecEncoding::F32,
5432            ),
5433            (
5434                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
5435                VecEncoding::Sq8,
5436            ),
5437            (
5438                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5439                VecEncoding::F16,
5440            ),
5441        ] {
5442            let s = parse(sql);
5443            let Statement::AlterIndex(a) = s else {
5444                panic!("{sql}: expected AlterIndex")
5445            };
5446            assert_eq!(a.name, "my_idx");
5447            assert_eq!(
5448                a.target,
5449                AlterIndexTarget::Rebuild {
5450                    encoding: Some(want)
5451                },
5452                "{sql}"
5453            );
5454        }
5455    }
5456
5457    #[test]
5458    fn alter_index_rebuild_unknown_encoding_errors() {
5459        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
5460        assert!(
5461            err.message.contains("unknown vector encoding"),
5462            "got: {}",
5463            err.message
5464        );
5465    }
5466
5467    #[test]
5468    fn alter_index_rebuild_display_roundtrips() {
5469        for (input, want) in [
5470            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
5471            (
5472                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
5473                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
5474            ),
5475            (
5476                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5477                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
5478            ),
5479        ] {
5480            let s = parse(input);
5481            assert_eq!(s.to_string(), want);
5482        }
5483    }
5484
5485    #[test]
5486    fn create_table_unknown_type_errors() {
5487        // v4.9: JSON is now real; pick an actually unsupported keyword
5488        // (XML never landed and isn't planned).
5489        let err = parse_statement("CREATE TABLE x (a xml)").unwrap_err();
5490        assert!(err.message.contains("unsupported column type"));
5491    }
5492
5493    #[test]
5494    fn create_table_missing_table_keyword_errors() {
5495        assert!(parse_statement("CREATE x (a INT)").is_err());
5496    }
5497
5498    #[test]
5499    fn insert_single_value() {
5500        let s = parse("INSERT INTO foo VALUES (42)");
5501        let Statement::Insert(i) = s else {
5502            panic!("expected Insert")
5503        };
5504        assert_eq!(i.table, "foo");
5505        assert_eq!(i.rows.len(), 1);
5506        assert_eq!(i.rows[0].len(), 1);
5507        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
5508    }
5509
5510    #[test]
5511    fn insert_multi_value_with_mixed_literals() {
5512        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
5513        let Statement::Insert(i) = s else { panic!() };
5514        assert_eq!(i.rows.len(), 1);
5515        assert_eq!(i.rows[0].len(), 5);
5516    }
5517
5518    #[test]
5519    fn insert_missing_into_errors() {
5520        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
5521    }
5522
5523    #[test]
5524    fn create_table_round_trip() {
5525        let original =
5526            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
5527        let text = original.to_string();
5528        let again = parse_statement(&text).expect("re-parse");
5529        assert_eq!(original, again);
5530    }
5531
5532    #[test]
5533    fn insert_round_trip_with_negation_and_string() {
5534        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
5535        let text = original.to_string();
5536        let again = parse_statement(&text).expect("re-parse");
5537        assert_eq!(original, again);
5538    }
5539
5540    #[test]
5541    fn unknown_keyword_at_statement_start_errors() {
5542        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
5543        // the top-level dispatch still has no branch to take.
5544        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
5545        assert!(err.message.contains("expected SELECT"));
5546    }
5547
5548    // --- v0.8 CREATE INDEX --------------------------------------------------
5549
5550    #[test]
5551    fn create_index_basic() {
5552        let s = parse("CREATE INDEX idx_id ON users (id)");
5553        let Statement::CreateIndex(c) = s else {
5554            panic!("expected CreateIndex")
5555        };
5556        assert_eq!(c.name, "idx_id");
5557        assert_eq!(c.table, "users");
5558        assert_eq!(c.column, "id");
5559    }
5560
5561    #[test]
5562    fn create_index_missing_on_errors() {
5563        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
5564    }
5565
5566    #[test]
5567    fn create_index_missing_paren_errors() {
5568        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
5569    }
5570
5571    #[test]
5572    fn create_index_round_trip() {
5573        let original = parse("CREATE INDEX by_name ON users (name)");
5574        let again = parse_statement(&original.to_string()).unwrap();
5575        assert_eq!(original, again);
5576    }
5577
5578    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
5579
5580    #[test]
5581    fn create_unique_index_basic() {
5582        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
5583        let Statement::CreateIndex(c) = s else {
5584            panic!("expected CreateIndex");
5585        };
5586        assert!(c.is_unique);
5587        assert_eq!(c.column, "a");
5588        assert!(c.partial_predicate.is_none());
5589    }
5590
5591    #[test]
5592    fn create_unique_index_partial() {
5593        // mailrs's email_templates "one default per user" shape.
5594        let s = parse(
5595            "CREATE UNIQUE INDEX idx_email_templates_user_default \
5596             ON email_templates (user_address) WHERE is_default = true",
5597        );
5598        let Statement::CreateIndex(c) = s else {
5599            panic!("expected CreateIndex");
5600        };
5601        assert!(c.is_unique);
5602        assert_eq!(c.table, "email_templates");
5603        assert_eq!(c.column, "user_address");
5604        assert!(c.partial_predicate.is_some());
5605    }
5606
5607    #[test]
5608    fn create_unique_index_composite_with_predicate() {
5609        // mailrs's calendar_events instance: composite columns.
5610        let s = parse(
5611            "CREATE UNIQUE INDEX uq_calendar_events_instance \
5612             ON calendar_events (calendar_id, uid, recurrence_id) \
5613             WHERE recurrence_id IS NOT NULL",
5614        );
5615        let Statement::CreateIndex(c) = s else {
5616            panic!("expected CreateIndex");
5617        };
5618        assert!(c.is_unique);
5619        assert_eq!(c.column, "calendar_id");
5620        assert_eq!(
5621            c.extra_columns,
5622            vec!["uid".to_string(), "recurrence_id".to_string()]
5623        );
5624        assert!(c.partial_predicate.is_some());
5625    }
5626
5627    #[test]
5628    fn create_unique_index_using_btree_ok() {
5629        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
5630        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
5631    }
5632
5633    #[test]
5634    fn create_unique_index_using_hnsw_rejected() {
5635        let err =
5636            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
5637        assert!(err.message.contains("UNIQUE"), "{}", err.message);
5638    }
5639
5640    #[test]
5641    fn create_unique_index_round_trip() {
5642        let original = parse(
5643            "CREATE UNIQUE INDEX uq_calendar_events_master \
5644             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
5645        );
5646        let again = parse_statement(&original.to_string()).unwrap();
5647        assert_eq!(original, again);
5648    }
5649
5650    #[test]
5651    fn create_unique_without_index_errors() {
5652        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
5653        assert!(err.message.contains("INDEX"), "{}", err.message);
5654    }
5655
5656    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
5657
5658    #[test]
5659    fn create_table_bytea_column() {
5660        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
5661        let Statement::CreateTable(c) = s else {
5662            panic!("expected CreateTable");
5663        };
5664        assert_eq!(c.columns.len(), 2);
5665        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
5666        assert!(!c.columns[1].nullable);
5667    }
5668
5669    #[test]
5670    fn create_table_bytes_alias_column() {
5671        let s = parse("CREATE TABLE t (blob BYTES)");
5672        let Statement::CreateTable(c) = s else {
5673            panic!("expected CreateTable");
5674        };
5675        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
5676    }
5677
5678    #[test]
5679    fn bytea_round_trip_display() {
5680        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
5681        let again = parse_statement(&original.to_string()).unwrap();
5682        assert_eq!(original, again);
5683    }
5684
5685    // --- v0.9 transactions -------------------------------------------------
5686
5687    #[test]
5688    fn begin_commit_rollback_parse_as_unit_variants() {
5689        assert_eq!(parse("BEGIN"), Statement::Begin);
5690        assert_eq!(parse("COMMIT"), Statement::Commit);
5691        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
5692        // Trailing semicolons accepted too.
5693        assert_eq!(parse("BEGIN;"), Statement::Begin);
5694    }
5695
5696    // --- v1.2: pgvector distance ops + ::vector cast --------------------
5697
5698    #[test]
5699    fn inner_product_binop_parses() {
5700        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
5701        let Statement::Select(s) = s else { panic!() };
5702        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5703            panic!()
5704        };
5705        assert!(matches!(
5706            expr,
5707            Expr::Binary {
5708                op: BinOp::InnerProduct,
5709                ..
5710            }
5711        ));
5712    }
5713
5714    #[test]
5715    fn cosine_distance_binop_parses() {
5716        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
5717        let Statement::Select(s) = s else { panic!() };
5718        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5719            panic!()
5720        };
5721        assert!(matches!(
5722            expr,
5723            Expr::Binary {
5724                op: BinOp::CosineDistance,
5725                ..
5726            }
5727        ));
5728    }
5729
5730    #[test]
5731    fn vector_cast_postfix_wraps_string_literal() {
5732        let s = parse("SELECT '[1,2,3]'::vector FROM t");
5733        let Statement::Select(s) = s else { panic!() };
5734        let SelectItem::Expr { expr, .. } = &s.items[0] else {
5735            panic!()
5736        };
5737        assert!(matches!(
5738            expr,
5739            Expr::Cast {
5740                target: CastTarget::Vector,
5741                ..
5742            }
5743        ));
5744    }
5745
5746    #[test]
5747    fn unsupported_cast_target_errors() {
5748        // `::numeric` isn't in the v1.3 cast target set.
5749        let err = parse_statement("SELECT 1::numeric FROM t").unwrap_err();
5750        assert!(err.message.contains("unsupported cast target"));
5751    }
5752
5753    #[test]
5754    fn tx_statements_round_trip() {
5755        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
5756            let original = parse(q);
5757            let again = parse_statement(&original.to_string()).unwrap();
5758            assert_eq!(original, again);
5759        }
5760    }
5761
5762    #[test]
5763    fn interval_text_parsing_units() {
5764        // Single unit.
5765        assert_eq!(parse_interval_text("1 day"), Some((0, 86_400_000_000)));
5766        assert_eq!(parse_interval_text("1 second"), Some((0, 1_000_000)));
5767        assert_eq!(parse_interval_text("1 month"), Some((1, 0)));
5768        assert_eq!(parse_interval_text("2 years"), Some((24, 0)));
5769        // Compound spans accumulate.
5770        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0)));
5771        assert_eq!(
5772            parse_interval_text("1 day 2 hours"),
5773            Some((0, 86_400_000_000 + 7_200_000_000))
5774        );
5775        // Negative numbers carry through.
5776        assert_eq!(parse_interval_text("-1 day"), Some((0, -86_400_000_000)));
5777        // Bad shapes return None.
5778        assert_eq!(parse_interval_text(""), None);
5779        assert_eq!(parse_interval_text("garbage"), None);
5780        assert_eq!(parse_interval_text("1 fortnight"), None);
5781        assert_eq!(parse_interval_text("1"), None);
5782    }
5783
5784    #[test]
5785    fn interval_literal_roundtrips_via_display() {
5786        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
5787        let s = parsed.to_string();
5788        // Display preserves the original text verbatim.
5789        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
5790        // And re-parsing yields a structurally equal statement.
5791        let again = parse_statement(&s).unwrap();
5792        assert_eq!(parsed, again);
5793    }
5794
5795    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
5796
5797    #[test]
5798    fn parser_recognises_create_publication_bare() {
5799        let s = parse("CREATE PUBLICATION pub_a");
5800        let Statement::CreatePublication(p) = s else {
5801            panic!("expected CreatePublication, got {s:?}")
5802        };
5803        assert_eq!(p.name, "pub_a");
5804        assert_eq!(p.scope, PublicationScope::AllTables);
5805    }
5806
5807    #[test]
5808    fn parser_recognises_create_publication_for_all_tables() {
5809        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
5810        let Statement::CreatePublication(p) = s else {
5811            panic!("expected CreatePublication, got {s:?}")
5812        };
5813        assert_eq!(p.name, "pub_a");
5814        assert_eq!(p.scope, PublicationScope::AllTables);
5815    }
5816
5817    #[test]
5818    fn parser_recognises_drop_publication() {
5819        let s = parse("DROP PUBLICATION pub_a");
5820        let Statement::DropPublication(name) = s else {
5821            panic!("expected DropPublication, got {s:?}")
5822        };
5823        assert_eq!(name, "pub_a");
5824    }
5825
5826    #[test]
5827    fn parser_recognises_for_table_list() {
5828        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
5829        let Statement::CreatePublication(p) = s else {
5830            panic!("expected CreatePublication, got {s:?}")
5831        };
5832        assert_eq!(p.name, "pub_a");
5833        let PublicationScope::ForTables(ts) = p.scope else {
5834            panic!("expected ForTables scope")
5835        };
5836        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
5837    }
5838
5839    #[test]
5840    fn parser_recognises_for_tables_plural() {
5841        // PG 19 accepts both `FOR TABLE` and `FOR TABLES` — match.
5842        let s = parse("CREATE PUBLICATION pub_a FOR TABLES t1, t2");
5843        let Statement::CreatePublication(p) = s else {
5844            panic!("expected CreatePublication, got {s:?}")
5845        };
5846        let PublicationScope::ForTables(ts) = p.scope else {
5847            panic!("expected ForTables")
5848        };
5849        assert_eq!(ts, alloc::vec!["t1", "t2"]);
5850    }
5851
5852    #[test]
5853    fn parser_recognises_for_all_tables_except_list() {
5854        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
5855        let Statement::CreatePublication(p) = s else {
5856            panic!()
5857        };
5858        let PublicationScope::AllTablesExcept(ts) = p.scope else {
5859            panic!("expected AllTablesExcept")
5860        };
5861        assert_eq!(ts, alloc::vec!["t1", "t2"]);
5862    }
5863
5864    #[test]
5865    fn parser_rejects_for_table_with_empty_list() {
5866        // `FOR TABLE` with nothing after is a parse error.
5867        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
5868            .expect_err("must error on empty list");
5869        // No specific message asserted — the call falls through to
5870        // expect_ident_like which yields "expected identifier, got …".
5871        assert!(!err.message.is_empty());
5872    }
5873
5874    #[test]
5875    fn parser_recognises_show_publications() {
5876        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
5877        // bare ident in this position, NOT a reserved keyword.
5878        let s = parse("SHOW PUBLICATIONS");
5879        assert!(matches!(s, Statement::ShowPublications));
5880    }
5881
5882    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
5883
5884    #[test]
5885    fn parser_recognises_create_subscription_single_publication() {
5886        let s = parse(
5887            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
5888        );
5889        let Statement::CreateSubscription(c) = s else {
5890            panic!("expected CreateSubscription, got {s:?}")
5891        };
5892        assert_eq!(c.name, "sub_a");
5893        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
5894        assert_eq!(c.publications, alloc::vec!["pub_a"]);
5895    }
5896
5897    #[test]
5898    fn parser_recognises_create_subscription_multi_publication() {
5899        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
5900        let Statement::CreateSubscription(c) = s else {
5901            panic!()
5902        };
5903        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
5904    }
5905
5906    #[test]
5907    fn parser_rejects_create_subscription_missing_connection() {
5908        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
5909            .expect_err("must error on missing CONNECTION");
5910        assert!(err.message.contains("CONNECTION"), "got: {}", err.message);
5911    }
5912
5913    #[test]
5914    fn parser_rejects_create_subscription_missing_publication() {
5915        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
5916            .expect_err("must error on missing PUBLICATION");
5917        assert!(err.message.contains("PUBLICATION"), "got: {}", err.message);
5918    }
5919
5920    #[test]
5921    fn parser_recognises_drop_subscription() {
5922        let s = parse("DROP SUBSCRIPTION sub_a");
5923        let Statement::DropSubscription(name) = s else {
5924            panic!("expected DropSubscription, got {s:?}")
5925        };
5926        assert_eq!(name, "sub_a");
5927    }
5928
5929    #[test]
5930    fn parser_recognises_show_subscriptions() {
5931        let s = parse("SHOW SUBSCRIPTIONS");
5932        assert!(matches!(s, Statement::ShowSubscriptions));
5933    }
5934
5935    #[test]
5936    fn parser_recognises_wait_for_wal_position_no_timeout() {
5937        let s = parse("WAIT FOR WAL POSITION 12345");
5938        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
5939            panic!("expected WaitForWalPosition, got {s:?}")
5940        };
5941        assert_eq!(pos, 12345);
5942        assert!(timeout_ms.is_none());
5943    }
5944
5945    #[test]
5946    fn parser_recognises_wait_for_wal_position_with_timeout() {
5947        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
5948        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
5949            panic!()
5950        };
5951        assert_eq!(pos, 67890);
5952        assert_eq!(timeout_ms, Some(5000));
5953    }
5954
5955    #[test]
5956    fn parser_rejects_wait_with_negative_position() {
5957        // The lexer treats `-` as a token; `expect_u64_literal`
5958        // only sees the Integer that follows, so the negative
5959        // arrives as a unary-minus expression at higher levels.
5960        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
5961        // parse error one way or another.
5962        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
5963        assert!(!err.message.is_empty());
5964    }
5965
5966    #[test]
5967    fn parser_recognises_bare_analyze() {
5968        let s = parse("ANALYZE");
5969        assert!(matches!(s, Statement::Analyze(None)));
5970    }
5971
5972    #[test]
5973    fn parser_recognises_analyze_with_table() {
5974        let s = parse("ANALYZE users");
5975        let Statement::Analyze(Some(name)) = s else {
5976            panic!("expected Analyze, got {s:?}")
5977        };
5978        assert_eq!(name, "users");
5979    }
5980
5981    #[test]
5982    fn parser_recognises_analyze_with_quoted_table() {
5983        let s = parse("ANALYZE \"Mixed Case\"");
5984        let Statement::Analyze(Some(name)) = s else {
5985            panic!()
5986        };
5987        assert_eq!(name, "Mixed Case");
5988    }
5989
5990    #[test]
5991    fn parser_rejects_analyze_with_garbage_token() {
5992        let err = parse_statement("ANALYZE 42").expect_err("must error");
5993        assert!(!err.message.is_empty());
5994    }
5995
5996    #[test]
5997    fn analyze_display_roundtrips() {
5998        for sql in ["ANALYZE", "ANALYZE users"] {
5999            let s = parse(sql);
6000            let printed = s.to_string();
6001            let again = parse_statement(&printed)
6002                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6003            assert_eq!(s, again);
6004        }
6005    }
6006
6007    #[test]
6008    fn wait_for_display_roundtrips() {
6009        for sql in [
6010            "WAIT FOR WAL POSITION 12345",
6011            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
6012        ] {
6013            let s = parse(sql);
6014            let printed = s.to_string();
6015            let again = parse_statement(&printed)
6016                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6017            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6018        }
6019    }
6020
6021    #[test]
6022    fn subscription_ddl_display_roundtrips() {
6023        for sql in [
6024            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
6025            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
6026            "DROP SUBSCRIPTION sub_a",
6027            "SHOW SUBSCRIPTIONS",
6028        ] {
6029            let s = parse(sql);
6030            let printed = s.to_string();
6031            let again = parse_statement(&printed)
6032                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6033            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6034        }
6035    }
6036
6037    #[test]
6038    fn parser_drop_dispatches_user_vs_publication() {
6039        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
6040        // tokenises DROP. Both targets must still parse.
6041        let s = parse("DROP USER 'alice'");
6042        let Statement::DropUser(name) = s else {
6043            panic!("expected DropUser, got {s:?}")
6044        };
6045        assert_eq!(name, "alice");
6046        // And DROP PUBLICATION lands the new variant.
6047        let s = parse("DROP PUBLICATION p1");
6048        assert!(matches!(s, Statement::DropPublication(_)));
6049    }
6050
6051    #[test]
6052    fn publication_ddl_display_roundtrips() {
6053        // Every CREATE PUBLICATION variant must Display → parse →
6054        // same AST. v6.1.3 covers all three scope shapes.
6055        for sql in [
6056            "CREATE PUBLICATION pub_a",
6057            "CREATE PUBLICATION pub_a FOR ALL TABLES",
6058            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
6059            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
6060            "DROP PUBLICATION pub_a",
6061            "SHOW PUBLICATIONS",
6062        ] {
6063            let s = parse(sql);
6064            let printed = s.to_string();
6065            let again = parse_statement(&printed)
6066                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6067            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6068        }
6069    }
6070
6071    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
6072
6073    #[test]
6074    fn create_function_returns_trigger_plpgsql_minimal() {
6075        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
6076        let s = parse(sql);
6077        let Statement::CreateFunction(f) = s else {
6078            panic!("expected CreateFunction");
6079        };
6080        assert_eq!(f.name, "noop");
6081        assert!(!f.or_replace);
6082        assert!(f.args.is_empty());
6083        assert!(matches!(f.returns, FunctionReturn::Trigger));
6084        assert_eq!(f.language, "plpgsql");
6085        let FunctionBody::PlPgSql(block) = f.body else {
6086            panic!("expected PlPgSql body");
6087        };
6088        assert_eq!(block.statements.len(), 1);
6089        assert!(matches!(
6090            block.statements[0],
6091            PlPgSqlStmt::Return(ReturnTarget::New)
6092        ));
6093    }
6094
6095    #[test]
6096    fn create_function_or_replace_with_assignment() {
6097        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
6098        // RETURN NEW.
6099        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
6100BEGIN
6101  NEW.search_vector := to_tsvector('english', NEW.subject);
6102  RETURN NEW;
6103END;
6104$$";
6105        let s = parse(sql);
6106        let Statement::CreateFunction(f) = s else {
6107            panic!("expected CreateFunction");
6108        };
6109        assert!(f.or_replace);
6110        let FunctionBody::PlPgSql(block) = &f.body else {
6111            panic!("expected PlPgSql body");
6112        };
6113        assert_eq!(block.statements.len(), 2);
6114        // First statement: NEW.search_vector := to_tsvector(...)
6115        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
6116            panic!("expected Assign as first stmt");
6117        };
6118        match target {
6119            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
6120            other => panic!("expected NEW.col, got {other:?}"),
6121        }
6122        // Second statement: RETURN NEW
6123        assert!(matches!(
6124            block.statements[1],
6125            PlPgSqlStmt::Return(ReturnTarget::New)
6126        ));
6127    }
6128
6129    #[test]
6130    fn create_trigger_after_insert_or_update() {
6131        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
6132        let s = parse(sql);
6133        let Statement::CreateTrigger(t) = s else {
6134            panic!("expected CreateTrigger");
6135        };
6136        assert_eq!(t.name, "tg");
6137        assert_eq!(t.table, "messages");
6138        assert_eq!(t.timing, TriggerTiming::After);
6139        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
6140        assert_eq!(t.for_each, TriggerForEach::Row);
6141        assert_eq!(t.function, "update_sv");
6142    }
6143
6144    #[test]
6145    fn create_trigger_before_delete_execute_procedure_alias() {
6146        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
6147        let sql =
6148            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
6149        let s = parse(sql);
6150        let Statement::CreateTrigger(t) = s else {
6151            panic!("expected CreateTrigger");
6152        };
6153        assert_eq!(t.timing, TriggerTiming::Before);
6154        assert_eq!(t.events, vec![TriggerEvent::Delete]);
6155    }
6156
6157    #[test]
6158    fn drop_trigger_if_exists_round_trips() {
6159        // No parser support for DROP TRIGGER yet — added in v7.12.5
6160        // alongside the broader DROP …{IF EXISTS} cleanup. The
6161        // AST + Display impls are in place so we round-trip via
6162        // construction:
6163        let s = Statement::DropTrigger {
6164            name: "tg".into(),
6165            table: "messages".into(),
6166            if_exists: true,
6167        };
6168        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
6169    }
6170
6171    #[test]
6172    fn trigger_ddl_display_roundtrips_through_parser() {
6173        // CREATE TRIGGER + its referenced CREATE FUNCTION must
6174        // Display → parse → same AST (modulo PL/pgSQL body
6175        // formatting which is parser-canonicalised).
6176        for sql in [
6177            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
6178            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
6179        ] {
6180            let s = parse(sql);
6181            let printed = s.to_string();
6182            let again = parse_statement(&printed)
6183                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
6184            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
6185        }
6186    }
6187}