Skip to main content

spg_sql/
parser.rs

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