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