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