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