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