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